Skip to content

Commit f0afc37

Browse files
authored
feat(node): Add trace context to checkin (#8503)
This PR adds `trace` context to checkin bodies as well as to the checkin envelope header.
1 parent 1167436 commit f0afc37

File tree

8 files changed

+147
-20
lines changed

8 files changed

+147
-20
lines changed

packages/core/src/checkin.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,42 @@
1-
import type { CheckInEvelope, CheckInItem, DsnComponents, SdkMetadata, SerializedCheckIn } from '@sentry/types';
2-
import { createEnvelope, dsnToString } from '@sentry/utils';
1+
import type {
2+
CheckInEvelope,
3+
CheckInItem,
4+
DsnComponents,
5+
DynamicSamplingContext,
6+
SdkMetadata,
7+
SerializedCheckIn,
8+
} from '@sentry/types';
9+
import { createEnvelope, dropUndefinedKeys, dsnToString } from '@sentry/utils';
310

411
/**
512
* Create envelope from check in item.
613
*/
714
export function createCheckInEnvelope(
815
checkIn: SerializedCheckIn,
16+
dynamicSamplingContext?: Partial<DynamicSamplingContext>,
917
metadata?: SdkMetadata,
1018
tunnel?: string,
1119
dsn?: DsnComponents,
1220
): CheckInEvelope {
1321
const headers: CheckInEvelope[0] = {
1422
sent_at: new Date().toISOString(),
15-
...(metadata &&
16-
metadata.sdk && {
17-
sdk: {
18-
name: metadata.sdk.name,
19-
version: metadata.sdk.version,
20-
},
21-
}),
22-
...(!!tunnel && !!dsn && { dsn: dsnToString(dsn) }),
2323
};
24+
25+
if (metadata && metadata.sdk) {
26+
headers.sdk = {
27+
name: metadata.sdk.name,
28+
version: metadata.sdk.version,
29+
};
30+
}
31+
32+
if (!!tunnel && !!dsn) {
33+
headers.dsn = dsnToString(dsn);
34+
}
35+
36+
if (dynamicSamplingContext) {
37+
headers.trace = dropUndefinedKeys(dynamicSamplingContext) as DynamicSamplingContext;
38+
}
39+
2440
const item = createCheckInEnvelopeItem(checkIn);
2541
return createEnvelope<CheckInEvelope>(headers, [item]);
2642
}

packages/core/src/exports.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,15 @@ export function startTransaction(
196196
* to create a monitor automatically when sending a check in.
197197
*/
198198
export function captureCheckIn(checkIn: CheckIn, upsertMonitorConfig?: MonitorConfig): string {
199-
const client = getCurrentHub().getClient();
199+
const hub = getCurrentHub();
200+
const scope = hub.getScope();
201+
const client = hub.getClient();
200202
if (!client) {
201203
__DEBUG_BUILD__ && logger.warn('Cannot capture check-in. No client defined.');
202204
} else if (!client.captureCheckIn) {
203205
__DEBUG_BUILD__ && logger.warn('Cannot capture check-in. Client does not support sending check-ins.');
204206
} else {
205-
return client.captureCheckIn(checkIn, upsertMonitorConfig);
207+
return client.captureCheckIn(checkIn, upsertMonitorConfig, scope);
206208
}
207209

208210
return uuid4();

packages/core/test/lib/checkin.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ describe('createCheckInEnvelope', () => {
1010
monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb',
1111
status: 'in_progress',
1212
},
13+
{
14+
trace_id: '86f39e84263a4de99c326acab3bfe3bd',
15+
public_key: 'testPublicKey',
16+
},
1317
{
1418
sdk: {
1519
name: 'testSdkName',
@@ -30,6 +34,10 @@ describe('createCheckInEnvelope', () => {
3034
name: 'testSdkName',
3135
version: 'testSdkVersion',
3236
},
37+
trace: {
38+
trace_id: '86f39e84263a4de99c326acab3bfe3bd',
39+
public_key: 'testPublicKey',
40+
},
3341
sent_at: expect.any(String),
3442
});
3543
});

packages/nextjs/src/edge/edgeclient.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
import type { Scope } from '@sentry/core';
2-
import { addTracingExtensions, BaseClient, createCheckInEnvelope, SDK_VERSION } from '@sentry/core';
2+
import {
3+
addTracingExtensions,
4+
BaseClient,
5+
createCheckInEnvelope,
6+
getDynamicSamplingContextFromClient,
7+
SDK_VERSION,
8+
} from '@sentry/core';
39
import type {
410
CheckIn,
511
ClientOptions,
12+
DynamicSamplingContext,
613
Event,
714
EventHint,
815
MonitorConfig,
916
SerializedCheckIn,
1017
Severity,
1118
SeverityLevel,
19+
TraceContext,
1220
} from '@sentry/types';
1321
import { logger, uuid4 } from '@sentry/utils';
1422

@@ -72,7 +80,7 @@ export class EdgeClient extends BaseClient<EdgeClientOptions> {
7280
* @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want
7381
* to create a monitor automatically when sending a check in.
7482
*/
75-
public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig): string {
83+
public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig, scope?: Scope): string {
7684
const id = checkIn.status !== 'in_progress' && checkIn.checkInId ? checkIn.checkInId : uuid4();
7785
if (!this._isEnabled()) {
7886
__DEBUG_BUILD__ && logger.warn('SDK not enabled, will not capture checkin.');
@@ -103,7 +111,20 @@ export class EdgeClient extends BaseClient<EdgeClientOptions> {
103111
};
104112
}
105113

106-
const envelope = createCheckInEnvelope(serializedCheckIn, this.getSdkMetadata(), tunnel, this.getDsn());
114+
const [dynamicSamplingContext, traceContext] = this._getTraceInfoFromScope(scope);
115+
if (traceContext) {
116+
serializedCheckIn.contexts = {
117+
trace: traceContext,
118+
};
119+
}
120+
121+
const envelope = createCheckInEnvelope(
122+
serializedCheckIn,
123+
dynamicSamplingContext,
124+
this.getSdkMetadata(),
125+
tunnel,
126+
this.getDsn(),
127+
);
107128

108129
__DEBUG_BUILD__ && logger.info('Sending checkin:', checkIn.monitorSlug, checkIn.status);
109130
void this._sendEnvelope(envelope);
@@ -124,4 +145,30 @@ export class EdgeClient extends BaseClient<EdgeClientOptions> {
124145
event.server_name = event.server_name || process.env.SENTRY_NAME;
125146
return super._prepareEvent(event, hint, scope);
126147
}
148+
149+
/** Extract trace information from scope */
150+
private _getTraceInfoFromScope(
151+
scope: Scope | undefined,
152+
): [dynamicSamplingContext: Partial<DynamicSamplingContext> | undefined, traceContext: TraceContext | undefined] {
153+
if (!scope) {
154+
return [undefined, undefined];
155+
}
156+
157+
const span = scope.getSpan();
158+
if (span) {
159+
return [span?.transaction?.getDynamicSamplingContext(), span?.getTraceContext()];
160+
}
161+
162+
const { traceId, spanId, parentSpanId, dsc } = scope.getPropagationContext();
163+
const traceContext: TraceContext = {
164+
trace_id: traceId,
165+
span_id: spanId,
166+
parent_span_id: parentSpanId,
167+
};
168+
if (dsc) {
169+
return [dsc, traceContext];
170+
}
171+
172+
return [getDynamicSamplingContextFromClient(traceId, this, scope), traceContext];
173+
}
127174
}

packages/node/src/client.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
import type { Scope } from '@sentry/core';
2-
import { addTracingExtensions, BaseClient, createCheckInEnvelope, SDK_VERSION, SessionFlusher } from '@sentry/core';
2+
import {
3+
addTracingExtensions,
4+
BaseClient,
5+
createCheckInEnvelope,
6+
getDynamicSamplingContextFromClient,
7+
SDK_VERSION,
8+
SessionFlusher,
9+
} from '@sentry/core';
310
import type {
411
CheckIn,
12+
DynamicSamplingContext,
513
Event,
614
EventHint,
715
MonitorConfig,
816
SerializedCheckIn,
917
Severity,
1018
SeverityLevel,
19+
TraceContext,
1120
} from '@sentry/types';
1221
import { logger, resolvedSyncPromise, uuid4 } from '@sentry/utils';
1322
import * as os from 'os';
@@ -154,7 +163,7 @@ export class NodeClient extends BaseClient<NodeClientOptions> {
154163
* to create a monitor automatically when sending a check in.
155164
* @returns A string representing the id of the check in.
156165
*/
157-
public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig): string {
166+
public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig, scope?: Scope): string {
158167
const id = checkIn.status !== 'in_progress' && checkIn.checkInId ? checkIn.checkInId : uuid4();
159168
if (!this._isEnabled()) {
160169
__DEBUG_BUILD__ && logger.warn('SDK not enabled, will not capture checkin.');
@@ -185,7 +194,20 @@ export class NodeClient extends BaseClient<NodeClientOptions> {
185194
};
186195
}
187196

188-
const envelope = createCheckInEnvelope(serializedCheckIn, this.getSdkMetadata(), tunnel, this.getDsn());
197+
const [dynamicSamplingContext, traceContext] = this._getTraceInfoFromScope(scope);
198+
if (traceContext) {
199+
serializedCheckIn.contexts = {
200+
trace: traceContext,
201+
};
202+
}
203+
204+
const envelope = createCheckInEnvelope(
205+
serializedCheckIn,
206+
dynamicSamplingContext,
207+
this.getSdkMetadata(),
208+
tunnel,
209+
this.getDsn(),
210+
);
189211

190212
__DEBUG_BUILD__ && logger.info('Sending checkin:', checkIn.monitorSlug, checkIn.status);
191213
void this._sendEnvelope(envelope);
@@ -220,4 +242,30 @@ export class NodeClient extends BaseClient<NodeClientOptions> {
220242
this._sessionFlusher.incrementSessionStatusCount();
221243
}
222244
}
245+
246+
/** Extract trace information from scope */
247+
private _getTraceInfoFromScope(
248+
scope: Scope | undefined,
249+
): [dynamicSamplingContext: Partial<DynamicSamplingContext> | undefined, traceContext: TraceContext | undefined] {
250+
if (!scope) {
251+
return [undefined, undefined];
252+
}
253+
254+
const span = scope.getSpan();
255+
if (span) {
256+
return [span?.transaction?.getDynamicSamplingContext(), span?.getTraceContext()];
257+
}
258+
259+
const { traceId, spanId, parentSpanId, dsc } = scope.getPropagationContext();
260+
const traceContext: TraceContext = {
261+
trace_id: traceId,
262+
span_id: spanId,
263+
parent_span_id: parentSpanId,
264+
};
265+
if (dsc) {
266+
return [dsc, traceContext];
267+
}
268+
269+
return [getDynamicSamplingContextFromClient(traceId, this, scope), traceContext];
270+
}
223271
}

packages/types/src/checkin.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { TraceContext } from './context';
2+
13
interface CrontabSchedule {
24
type: 'crontab';
35
// The crontab schedule string, e.g. 0 * * * *.
@@ -36,6 +38,9 @@ export interface SerializedCheckIn {
3638
// See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
3739
timezone?: string;
3840
};
41+
contexts?: {
42+
trace?: TraceContext;
43+
};
3944
}
4045

4146
interface InProgressCheckIn {

packages/types/src/client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,10 @@ export interface Client<O extends ClientOptions = ClientOptions> {
7474
* @param checkIn An object that describes a check in.
7575
* @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want
7676
* to create a monitor automatically when sending a check in.
77+
* @param scope An optional scope containing event metadata.
7778
* @returns A string representing the id of the check in.
7879
*/
79-
captureCheckIn?(checkIn: CheckIn, monitorConfig?: MonitorConfig): string;
80+
captureCheckIn?(checkIn: CheckIn, monitorConfig?: MonitorConfig, scope?: Scope): string;
8081

8182
/** Returns the current Dsn. */
8283
getDsn(): DsnComponents | undefined;

packages/types/src/envelope.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ type ReplayRecordingItem = BaseEnvelopeItem<ReplayRecordingItemHeaders, ReplayRe
8686

8787
export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: DynamicSamplingContext };
8888
type SessionEnvelopeHeaders = { sent_at: string };
89-
type CheckInEnvelopeHeaders = BaseEnvelopeHeaders;
89+
type CheckInEnvelopeHeaders = { trace?: DynamicSamplingContext };
9090
type ClientReportEnvelopeHeaders = BaseEnvelopeHeaders;
9191
type ReplayEnvelopeHeaders = BaseEnvelopeHeaders;
9292

0 commit comments

Comments
 (0)