Skip to content

Commit 5610ff3

Browse files
mydeac298lee
authored andcommitted
feat(core): Allow to pass start/end timestamp for spans flexibly (#10060)
We allow the same formats as OpenTelemetry: * `number` (we handle both seconds and milliseconds) * `Date` * `[seconds, nanoseconds]`
1 parent e783b33 commit 5610ff3

File tree

12 files changed

+198
-40
lines changed

12 files changed

+198
-40
lines changed

packages/core/src/tracing/idletransaction.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
/* eslint-disable max-lines */
2-
import type { TransactionContext } from '@sentry/types';
2+
import type { SpanTimeInput, TransactionContext } from '@sentry/types';
33
import { logger, timestampInSeconds } from '@sentry/utils';
44

55
import { DEBUG_BUILD } from '../debug-build';
66
import type { Hub } from '../hub';
7+
import { spanTimeInputToSeconds } from '../utils/spanUtils';
78
import type { Span } from './span';
89
import { SpanRecorder } from './span';
910
import { Transaction } from './transaction';
10-
import { ensureTimestampInSeconds } from './utils';
1111

1212
export const TRACING_DEFAULTS = {
1313
idleTimeout: 1000,
@@ -138,8 +138,8 @@ export class IdleTransaction extends Transaction {
138138
}
139139

140140
/** {@inheritDoc} */
141-
public end(endTimestamp: number = timestampInSeconds()): string | undefined {
142-
const endTimestampInS = ensureTimestampInSeconds(endTimestamp);
141+
public end(endTimestamp?: SpanTimeInput): string | undefined {
142+
const endTimestampInS = spanTimeInputToSeconds(endTimestamp);
143143

144144
this._finished = true;
145145
this.activities = {};
@@ -153,7 +153,7 @@ export class IdleTransaction extends Transaction {
153153
logger.log('[Tracing] finishing IdleTransaction', new Date(endTimestampInS * 1000).toISOString(), this.op);
154154

155155
for (const callback of this._beforeFinishCallbacks) {
156-
callback(this, endTimestamp);
156+
callback(this, endTimestampInS);
157157
}
158158

159159
this.spanRecorder.spans = this.spanRecorder.spans.filter((span: Span) => {

packages/core/src/tracing/span.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ import type {
77
SpanAttributes,
88
SpanContext,
99
SpanOrigin,
10+
SpanTimeInput,
1011
TraceContext,
1112
Transaction,
1213
} from '@sentry/types';
1314
import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils';
1415

1516
import { DEBUG_BUILD } from '../debug-build';
16-
import { spanToTraceContext, spanToTraceHeader } from '../utils/spanUtils';
17-
import { ensureTimestampInSeconds } from './utils';
17+
import { spanTimeInputToSeconds, spanToTraceContext, spanToTraceHeader } from '../utils/spanUtils';
1818

1919
/**
2020
* Keeps track of finished spans for a given transaction
@@ -300,7 +300,7 @@ export class Span implements SpanInterface {
300300
}
301301

302302
/** @inheritdoc */
303-
public end(endTimestamp?: number): void {
303+
public end(endTimestamp?: SpanTimeInput): void {
304304
if (
305305
DEBUG_BUILD &&
306306
// Don't call this for transactions
@@ -313,8 +313,7 @@ export class Span implements SpanInterface {
313313
}
314314
}
315315

316-
this.endTimestamp =
317-
typeof endTimestamp === 'number' ? ensureTimestampInSeconds(endTimestamp) : timestampInSeconds();
316+
this.endTimestamp = spanTimeInputToSeconds(endTimestamp);
318317
}
319318

320319
/**

packages/core/src/tracing/trace.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Span, TransactionContext } from '@sentry/types';
1+
import type { Span, SpanTimeInput, TransactionContext } from '@sentry/types';
22
import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils';
33

44
import { DEBUG_BUILD } from '../debug-build';
@@ -7,6 +7,12 @@ import type { Hub } from '../hub';
77
import { getCurrentHub } from '../hub';
88
import { handleCallbackErrors } from '../utils/handleCallbackErrors';
99
import { hasTracingEnabled } from '../utils/hasTracingEnabled';
10+
import { spanTimeInputToSeconds } from '../utils/spanUtils';
11+
12+
interface StartSpanOptions extends TransactionContext {
13+
/** A manually specified start time for the created `Span` object. */
14+
startTime?: SpanTimeInput;
15+
}
1016

1117
/**
1218
* Wraps a function with a transaction/span and finishes the span after the function is done.
@@ -65,7 +71,7 @@ export function trace<T>(
6571
* or you didn't set `tracesSampleRate`, this function will not generate spans
6672
* and the `span` returned from the callback will be undefined.
6773
*/
68-
export function startSpan<T>(context: TransactionContext, callback: (span: Span | undefined) => T): T {
74+
export function startSpan<T>(context: StartSpanOptions, callback: (span: Span | undefined) => T): T {
6975
const ctx = normalizeContext(context);
7076

7177
return withScope(scope => {
@@ -105,7 +111,7 @@ export const startActiveSpan = startSpan;
105111
* and the `span` returned from the callback will be undefined.
106112
*/
107113
export function startSpanManual<T>(
108-
context: TransactionContext,
114+
context: StartSpanOptions,
109115
callback: (span: Span | undefined, finish: () => void) => T,
110116
): T {
111117
const ctx = normalizeContext(context);
@@ -143,17 +149,12 @@ export function startSpanManual<T>(
143149
* or you didn't set `tracesSampleRate` or `tracesSampler`, this function will not generate spans
144150
* and the `span` returned from the callback will be undefined.
145151
*/
146-
export function startInactiveSpan(context: TransactionContext): Span | undefined {
152+
export function startInactiveSpan(context: StartSpanOptions): Span | undefined {
147153
if (!hasTracingEnabled()) {
148154
return undefined;
149155
}
150156

151-
const ctx = { ...context };
152-
// If a name is set and a description is not, set the description to the name.
153-
if (ctx.name !== undefined && ctx.description === undefined) {
154-
ctx.description = ctx.name;
155-
}
156-
157+
const ctx = normalizeContext(context);
157158
const hub = getCurrentHub();
158159
const parentSpan = getActiveSpan();
159160
return parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx);
@@ -238,12 +239,24 @@ function createChildSpanOrTransaction(
238239
return parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx);
239240
}
240241

241-
function normalizeContext(context: TransactionContext): TransactionContext {
242+
/**
243+
* This converts StartSpanOptions to TransactionContext.
244+
* For the most part (for now) we accept the same options,
245+
* but some of them need to be transformed.
246+
*
247+
* Eventually the StartSpanOptions will be more aligned with OpenTelemetry.
248+
*/
249+
function normalizeContext(context: StartSpanOptions): TransactionContext {
242250
const ctx = { ...context };
243251
// If a name is set and a description is not, set the description to the name.
244252
if (ctx.name !== undefined && ctx.description === undefined) {
245253
ctx.description = ctx.name;
246254
}
247255

256+
if (context.startTime) {
257+
ctx.startTimestamp = spanTimeInputToSeconds(context.startTime);
258+
delete ctx.startTime;
259+
}
260+
248261
return ctx;
249262
}

packages/core/src/tracing/transaction.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@ import type {
44
DynamicSamplingContext,
55
MeasurementUnit,
66
Measurements,
7+
SpanTimeInput,
78
Transaction as TransactionInterface,
89
TransactionContext,
910
TransactionEvent,
1011
TransactionMetadata,
1112
} from '@sentry/types';
12-
import { dropUndefinedKeys, logger, timestampInSeconds } from '@sentry/utils';
13+
import { dropUndefinedKeys, logger } from '@sentry/utils';
1314

1415
import { DEBUG_BUILD } from '../debug-build';
1516
import type { Hub } from '../hub';
1617
import { getCurrentHub } from '../hub';
17-
import { spanToTraceContext } from '../utils/spanUtils';
18+
import { spanTimeInputToSeconds, spanToTraceContext } from '../utils/spanUtils';
1819
import { getDynamicSamplingContextFromClient } from './dynamicSamplingContext';
1920
import { Span as SpanClass, SpanRecorder } from './span';
20-
import { ensureTimestampInSeconds } from './utils';
2121

2222
/** JSDoc */
2323
export class Transaction extends SpanClass implements TransactionInterface {
@@ -147,9 +147,8 @@ export class Transaction extends SpanClass implements TransactionInterface {
147147
/**
148148
* @inheritDoc
149149
*/
150-
public end(endTimestamp?: number): string | undefined {
151-
const timestampInS =
152-
typeof endTimestamp === 'number' ? ensureTimestampInSeconds(endTimestamp) : timestampInSeconds();
150+
public end(endTimestamp?: SpanTimeInput): string | undefined {
151+
const timestampInS = spanTimeInputToSeconds(endTimestamp);
153152
const transaction = this._finishTransaction(timestampInS);
154153
if (!transaction) {
155154
return undefined;

packages/core/src/tracing/utils.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,3 @@ export { stripUrlQueryAndFragment } from '@sentry/utils';
2727
* @deprecated Import this function from `@sentry/utils` instead
2828
*/
2929
export const extractTraceparentData = _extractTraceparentData;
30-
31-
/**
32-
* Converts a timestamp to second, if it was in milliseconds, or keeps it as second.
33-
*/
34-
export function ensureTimestampInSeconds(timestamp: number): number {
35-
const isMs = timestamp > 9999999999;
36-
return isMs ? timestamp / 1000 : timestamp;
37-
}

packages/core/src/utils/spanUtils.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { Span, TraceContext } from '@sentry/types';
2-
import { dropUndefinedKeys, generateSentryTraceHeader } from '@sentry/utils';
1+
import type { Span, SpanTimeInput, TraceContext } from '@sentry/types';
2+
import { dropUndefinedKeys, generateSentryTraceHeader, timestampInSeconds } from '@sentry/utils';
33

44
/**
55
* Convert a span to a trace context, which can be sent as the `trace` context in an event.
@@ -26,3 +26,31 @@ export function spanToTraceContext(span: Span): TraceContext {
2626
export function spanToTraceHeader(span: Span): string {
2727
return generateSentryTraceHeader(span.traceId, span.spanId, span.sampled);
2828
}
29+
30+
/**
31+
* Convert a span time input intp a timestamp in seconds.
32+
*/
33+
export function spanTimeInputToSeconds(input: SpanTimeInput | undefined): number {
34+
if (typeof input === 'number') {
35+
return ensureTimestampInSeconds(input);
36+
}
37+
38+
if (Array.isArray(input)) {
39+
// See {@link HrTime} for the array-based time format
40+
return input[0] + input[1] / 1e9;
41+
}
42+
43+
if (input instanceof Date) {
44+
return ensureTimestampInSeconds(input.getTime());
45+
}
46+
47+
return timestampInSeconds();
48+
}
49+
50+
/**
51+
* Converts a timestamp to second, if it was in milliseconds, or keeps it as second.
52+
*/
53+
function ensureTimestampInSeconds(timestamp: number): number {
54+
const isMs = timestamp > 9999999999;
55+
return isMs ? timestamp / 1000 : timestamp;
56+
}

packages/core/test/lib/tracing/span.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { timestampInSeconds } from '@sentry/utils';
12
import { Span } from '../../../src';
23

34
describe('span', () => {
@@ -174,6 +175,40 @@ describe('span', () => {
174175
});
175176
});
176177

178+
describe('end', () => {
179+
it('works without endTimestamp', () => {
180+
const span = new Span();
181+
const now = timestampInSeconds();
182+
span.end();
183+
184+
expect(span.endTimestamp).toBeGreaterThanOrEqual(now);
185+
});
186+
187+
it('works with endTimestamp in seconds', () => {
188+
const span = new Span();
189+
const timestamp = timestampInSeconds() - 1;
190+
span.end(timestamp);
191+
192+
expect(span.endTimestamp).toEqual(timestamp);
193+
});
194+
195+
it('works with endTimestamp in milliseconds', () => {
196+
const span = new Span();
197+
const timestamp = Date.now() - 1000;
198+
span.end(timestamp);
199+
200+
expect(span.endTimestamp).toEqual(timestamp / 1000);
201+
});
202+
203+
it('works with endTimestamp in array form', () => {
204+
const span = new Span();
205+
const seconds = Math.floor(timestampInSeconds() - 1);
206+
span.end([seconds, 0]);
207+
208+
expect(span.endTimestamp).toEqual(seconds);
209+
});
210+
});
211+
177212
// Ensure that attributes & data are merged together
178213
describe('_getData', () => {
179214
it('works without data & attributes', () => {

packages/core/test/lib/tracing/trace.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,14 @@ describe('startSpan', () => {
160160
expect(ref.spanRecorder.spans[1].status).toEqual(isError ? 'internal_error' : undefined);
161161
});
162162

163+
it('allows to pass a `startTime`', () => {
164+
const start = startSpan({ name: 'outer', startTime: [1234, 0] }, span => {
165+
return span?.startTimestamp;
166+
});
167+
168+
expect(start).toEqual(1234);
169+
});
170+
163171
it('allows for span to be mutated', async () => {
164172
let ref: any = undefined;
165173
client.on('finishTransaction', transaction => {
@@ -222,6 +230,15 @@ describe('startSpanManual', () => {
222230
expect(getCurrentScope()).toBe(initialScope);
223231
expect(initialScope.getSpan()).toBe(undefined);
224232
});
233+
234+
it('allows to pass a `startTime`', () => {
235+
const start = startSpanManual({ name: 'outer', startTime: [1234, 0] }, span => {
236+
span?.end();
237+
return span?.startTimestamp;
238+
});
239+
240+
expect(start).toEqual(1234);
241+
});
225242
});
226243

227244
describe('startInactiveSpan', () => {
@@ -248,6 +265,11 @@ describe('startInactiveSpan', () => {
248265

249266
expect(initialScope.getSpan()).toBeUndefined();
250267
});
268+
269+
it('allows to pass a `startTime`', () => {
270+
const span = startInactiveSpan({ name: 'outer', startTime: [1234, 0] });
271+
expect(span?.startTimestamp).toEqual(1234);
272+
});
251273
});
252274

253275
describe('continueTrace', () => {

packages/core/test/lib/utils/spanUtils.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { TRACEPARENT_REGEXP } from '@sentry/utils';
1+
import { TRACEPARENT_REGEXP, timestampInSeconds } from '@sentry/utils';
22
import { Span, spanToTraceHeader } from '../../../src';
3+
import { spanTimeInputToSeconds } from '../../../src/utils/spanUtils';
34

45
describe('spanToTraceHeader', () => {
56
test('simple', () => {
@@ -11,3 +12,37 @@ describe('spanToTraceHeader', () => {
1112
expect(spanToTraceHeader(span)).toMatch(TRACEPARENT_REGEXP);
1213
});
1314
});
15+
16+
describe('spanTimeInputToSeconds', () => {
17+
it('works with undefined', () => {
18+
const now = timestampInSeconds();
19+
expect(spanTimeInputToSeconds(undefined)).toBeGreaterThanOrEqual(now);
20+
});
21+
22+
it('works with a timestamp in seconds', () => {
23+
const timestamp = timestampInSeconds();
24+
expect(spanTimeInputToSeconds(timestamp)).toEqual(timestamp);
25+
});
26+
27+
it('works with a timestamp in milliseconds', () => {
28+
const timestamp = Date.now();
29+
expect(spanTimeInputToSeconds(timestamp)).toEqual(timestamp / 1000);
30+
});
31+
32+
it('works with a Date object', () => {
33+
const timestamp = new Date();
34+
expect(spanTimeInputToSeconds(timestamp)).toEqual(timestamp.getTime() / 1000);
35+
});
36+
37+
it('works with a simple array', () => {
38+
const seconds = Math.floor(timestampInSeconds());
39+
const timestamp: [number, number] = [seconds, 0];
40+
expect(spanTimeInputToSeconds(timestamp)).toEqual(seconds);
41+
});
42+
43+
it('works with a array with nanoseconds', () => {
44+
const seconds = Math.floor(timestampInSeconds());
45+
const timestamp: [number, number] = [seconds, 9000];
46+
expect(spanTimeInputToSeconds(timestamp)).toEqual(seconds + 0.000009);
47+
});
48+
});

packages/types/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export type {
8989

9090
// eslint-disable-next-line deprecation/deprecation
9191
export type { Severity, SeverityLevel } from './severity';
92-
export type { Span, SpanContext, SpanOrigin, SpanAttributeValue, SpanAttributes } from './span';
92+
export type { Span, SpanContext, SpanOrigin, SpanAttributeValue, SpanAttributes, SpanTimeInput } from './span';
9393
export type { StackFrame } from './stackframe';
9494
export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace';
9595
export type { TextEncoderInternal } from './textencoder';

0 commit comments

Comments
 (0)