Skip to content

Commit f54e121

Browse files
authored
feat(core): Add ServerRuntimeClient (#8930)
The `ServerRuntimeClient` is a near identical copy of the nextjs `EdgeClient`. To make it a direct replacement it has constructor options to override the event `platform`, `runtime`, and `server_name`. This PR makes yet another copy of the Node `eventbuilder.ts` but after future PRs to remove the `EdgeClient` and make `NodeClient` extend `ServerRuntimeClient`, this will be the only copy. I've put the `eventbuilder` code in utils since some of these functions are used elsewhere outside of the clients and I don't want to export these from core and them become part of our public API. This is especially important since the browser SDK already exports it's own slightly different `exceptionFromError`.
1 parent e2f0f4b commit f54e121

File tree

5 files changed

+463
-0
lines changed

5 files changed

+463
-0
lines changed

packages/core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export type { ClientClass } from './sdk';
22
export type { AsyncContextStrategy, Carrier, Layer, RunWithAsyncContextOptions } from './hub';
33
export type { OfflineStore, OfflineTransportOptions } from './transports/offline';
4+
export type { ServerRuntimeClientOptions } from './server-runtime-client';
45

56
export * from './tracing';
67
export {
@@ -38,6 +39,7 @@ export { SessionFlusher } from './sessionflusher';
3839
export { addGlobalEventProcessor, Scope } from './scope';
3940
export { getEnvelopeEndpointWithUrlEncodedAuth, getReportDialogEndpoint } from './api';
4041
export { BaseClient } from './baseclient';
42+
export { ServerRuntimeClient } from './server-runtime-client';
4143
export { initAndBind } from './sdk';
4244
export { createTransport } from './transports/base';
4345
export { makeOfflineTransport } from './transports/offline';
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import type {
2+
BaseTransportOptions,
3+
CheckIn,
4+
ClientOptions,
5+
DynamicSamplingContext,
6+
Event,
7+
EventHint,
8+
MonitorConfig,
9+
SerializedCheckIn,
10+
Severity,
11+
SeverityLevel,
12+
TraceContext,
13+
} from '@sentry/types';
14+
import { eventFromMessage, eventFromUnknownInput, logger, uuid4 } from '@sentry/utils';
15+
16+
import { BaseClient } from './baseclient';
17+
import { createCheckInEnvelope } from './checkin';
18+
import { getCurrentHub } from './hub';
19+
import type { Scope } from './scope';
20+
import { addTracingExtensions, getDynamicSamplingContextFromClient } from './tracing';
21+
22+
export interface ServerRuntimeClientOptions extends ClientOptions<BaseTransportOptions> {
23+
platform?: string;
24+
runtime?: { name: string; version?: string };
25+
serverName?: string;
26+
}
27+
28+
/**
29+
* The Sentry Server Runtime Client SDK.
30+
*/
31+
export class ServerRuntimeClient<
32+
O extends ClientOptions & ServerRuntimeClientOptions = ServerRuntimeClientOptions,
33+
> extends BaseClient<O> {
34+
/**
35+
* Creates a new Edge SDK instance.
36+
* @param options Configuration options for this SDK.
37+
*/
38+
public constructor(options: O) {
39+
// Server clients always support tracing
40+
addTracingExtensions();
41+
42+
super(options);
43+
}
44+
45+
/**
46+
* @inheritDoc
47+
*/
48+
public eventFromException(exception: unknown, hint?: EventHint): PromiseLike<Event> {
49+
return Promise.resolve(eventFromUnknownInput(getCurrentHub, this._options.stackParser, exception, hint));
50+
}
51+
52+
/**
53+
* @inheritDoc
54+
*/
55+
public eventFromMessage(
56+
message: string,
57+
// eslint-disable-next-line deprecation/deprecation
58+
level: Severity | SeverityLevel = 'info',
59+
hint?: EventHint,
60+
): PromiseLike<Event> {
61+
return Promise.resolve(
62+
eventFromMessage(this._options.stackParser, message, level, hint, this._options.attachStacktrace),
63+
);
64+
}
65+
66+
/**
67+
* Create a cron monitor check in and send it to Sentry.
68+
*
69+
* @param checkIn An object that describes a check in.
70+
* @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want
71+
* to create a monitor automatically when sending a check in.
72+
*/
73+
public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig, scope?: Scope): string {
74+
const id = checkIn.status !== 'in_progress' && checkIn.checkInId ? checkIn.checkInId : uuid4();
75+
if (!this._isEnabled()) {
76+
__DEBUG_BUILD__ && logger.warn('SDK not enabled, will not capture checkin.');
77+
return id;
78+
}
79+
80+
const options = this.getOptions();
81+
const { release, environment, tunnel } = options;
82+
83+
const serializedCheckIn: SerializedCheckIn = {
84+
check_in_id: id,
85+
monitor_slug: checkIn.monitorSlug,
86+
status: checkIn.status,
87+
release,
88+
environment,
89+
};
90+
91+
if (checkIn.status !== 'in_progress') {
92+
serializedCheckIn.duration = checkIn.duration;
93+
}
94+
95+
if (monitorConfig) {
96+
serializedCheckIn.monitor_config = {
97+
schedule: monitorConfig.schedule,
98+
checkin_margin: monitorConfig.checkinMargin,
99+
max_runtime: monitorConfig.maxRuntime,
100+
timezone: monitorConfig.timezone,
101+
};
102+
}
103+
104+
const [dynamicSamplingContext, traceContext] = this._getTraceInfoFromScope(scope);
105+
if (traceContext) {
106+
serializedCheckIn.contexts = {
107+
trace: traceContext,
108+
};
109+
}
110+
111+
const envelope = createCheckInEnvelope(
112+
serializedCheckIn,
113+
dynamicSamplingContext,
114+
this.getSdkMetadata(),
115+
tunnel,
116+
this.getDsn(),
117+
);
118+
119+
__DEBUG_BUILD__ && logger.info('Sending checkin:', checkIn.monitorSlug, checkIn.status);
120+
void this._sendEnvelope(envelope);
121+
return id;
122+
}
123+
124+
/**
125+
* @inheritDoc
126+
*/
127+
protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike<Event | null> {
128+
if (this._options.platform) {
129+
event.platform = event.platform || this._options.platform;
130+
}
131+
132+
if (this._options.runtime) {
133+
event.contexts = {
134+
...event.contexts,
135+
runtime: (event.contexts || {}).runtime || this._options.runtime,
136+
};
137+
}
138+
139+
if (this._options.serverName) {
140+
event.server_name = event.server_name || this._options.serverName;
141+
}
142+
143+
return super._prepareEvent(event, hint, scope);
144+
}
145+
146+
/** Extract trace information from scope */
147+
private _getTraceInfoFromScope(
148+
scope: Scope | undefined,
149+
): [dynamicSamplingContext: Partial<DynamicSamplingContext> | undefined, traceContext: TraceContext | undefined] {
150+
if (!scope) {
151+
return [undefined, undefined];
152+
}
153+
154+
const span = scope.getSpan();
155+
if (span) {
156+
const samplingContext = span.transaction ? span.transaction.getDynamicSamplingContext() : undefined;
157+
return [samplingContext, span.getTraceContext()];
158+
}
159+
160+
const { traceId, spanId, parentSpanId, dsc } = scope.getPropagationContext();
161+
const traceContext: TraceContext = {
162+
trace_id: traceId,
163+
span_id: spanId,
164+
parent_span_id: parentSpanId,
165+
};
166+
if (dsc) {
167+
return [dsc, traceContext];
168+
}
169+
170+
return [getDynamicSamplingContextFromClient(traceId, this, scope), traceContext];
171+
}
172+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import type { Event, EventHint } from '@sentry/types';
2+
3+
import { createTransport } from '../../src';
4+
import type { ServerRuntimeClientOptions } from '../../src/server-runtime-client';
5+
import { ServerRuntimeClient } from '../../src/server-runtime-client';
6+
7+
const PUBLIC_DSN = 'https://username@domain/123';
8+
9+
function getDefaultClientOptions(options: Partial<ServerRuntimeClientOptions> = {}): ServerRuntimeClientOptions {
10+
return {
11+
integrations: [],
12+
transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})),
13+
stackParser: () => [],
14+
instrumenter: 'sentry',
15+
...options,
16+
};
17+
}
18+
19+
describe('ServerRuntimeClient', () => {
20+
let client: ServerRuntimeClient;
21+
22+
describe('_prepareEvent', () => {
23+
test('adds platform to event', () => {
24+
const options = getDefaultClientOptions({ dsn: PUBLIC_DSN });
25+
const client = new ServerRuntimeClient({ ...options, platform: 'edge' });
26+
27+
const event: Event = {};
28+
const hint: EventHint = {};
29+
(client as any)._prepareEvent(event, hint);
30+
31+
expect(event.platform).toEqual('edge');
32+
});
33+
34+
test('adds server_name to event', () => {
35+
const options = getDefaultClientOptions({ dsn: PUBLIC_DSN });
36+
const client = new ServerRuntimeClient({ ...options, serverName: 'server' });
37+
38+
const event: Event = {};
39+
const hint: EventHint = {};
40+
(client as any)._prepareEvent(event, hint);
41+
42+
expect(event.server_name).toEqual('server');
43+
});
44+
45+
test('adds runtime context to event', () => {
46+
const options = getDefaultClientOptions({ dsn: PUBLIC_DSN });
47+
const client = new ServerRuntimeClient({ ...options, runtime: { name: 'edge' } });
48+
49+
const event: Event = {};
50+
const hint: EventHint = {};
51+
(client as any)._prepareEvent(event, hint);
52+
53+
expect(event.contexts?.runtime).toEqual({
54+
name: 'edge',
55+
});
56+
});
57+
58+
test("doesn't clobber existing runtime data", () => {
59+
const options = getDefaultClientOptions({ dsn: PUBLIC_DSN });
60+
const client = new ServerRuntimeClient({ ...options, runtime: { name: 'edge' } });
61+
62+
const event: Event = { contexts: { runtime: { name: 'foo', version: '1.2.3' } } };
63+
const hint: EventHint = {};
64+
(client as any)._prepareEvent(event, hint);
65+
66+
expect(event.contexts?.runtime).toEqual({ name: 'foo', version: '1.2.3' });
67+
expect(event.contexts?.runtime).not.toEqual({ name: 'edge' });
68+
});
69+
});
70+
71+
describe('captureCheckIn', () => {
72+
it('sends a checkIn envelope', () => {
73+
const options = getDefaultClientOptions({
74+
dsn: PUBLIC_DSN,
75+
serverName: 'bar',
76+
release: '1.0.0',
77+
environment: 'dev',
78+
});
79+
client = new ServerRuntimeClient(options);
80+
81+
// @ts-ignore accessing private method
82+
const sendEnvelopeSpy = jest.spyOn(client, '_sendEnvelope');
83+
84+
const id = client.captureCheckIn(
85+
{ monitorSlug: 'foo', status: 'in_progress' },
86+
{
87+
schedule: {
88+
type: 'crontab',
89+
value: '0 * * * *',
90+
},
91+
checkinMargin: 2,
92+
maxRuntime: 12333,
93+
timezone: 'Canada/Eastern',
94+
},
95+
);
96+
97+
expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1);
98+
expect(sendEnvelopeSpy).toHaveBeenCalledWith([
99+
expect.any(Object),
100+
[
101+
[
102+
expect.any(Object),
103+
{
104+
check_in_id: id,
105+
monitor_slug: 'foo',
106+
status: 'in_progress',
107+
release: '1.0.0',
108+
environment: 'dev',
109+
monitor_config: {
110+
schedule: {
111+
type: 'crontab',
112+
value: '0 * * * *',
113+
},
114+
checkin_margin: 2,
115+
max_runtime: 12333,
116+
timezone: 'Canada/Eastern',
117+
},
118+
},
119+
],
120+
],
121+
]);
122+
123+
client.captureCheckIn({ monitorSlug: 'foo', status: 'ok', duration: 1222, checkInId: id });
124+
125+
expect(sendEnvelopeSpy).toHaveBeenCalledTimes(2);
126+
expect(sendEnvelopeSpy).toHaveBeenCalledWith([
127+
expect.any(Object),
128+
[
129+
[
130+
expect.any(Object),
131+
{
132+
check_in_id: id,
133+
monitor_slug: 'foo',
134+
duration: 1222,
135+
status: 'ok',
136+
release: '1.0.0',
137+
environment: 'dev',
138+
},
139+
],
140+
],
141+
]);
142+
});
143+
144+
it('does not send a checkIn envelope if disabled', () => {
145+
const options = getDefaultClientOptions({ dsn: PUBLIC_DSN, serverName: 'bar', enabled: false });
146+
client = new ServerRuntimeClient(options);
147+
148+
// @ts-ignore accessing private method
149+
const sendEnvelopeSpy = jest.spyOn(client, '_sendEnvelope');
150+
151+
client.captureCheckIn({ monitorSlug: 'foo', status: 'in_progress' });
152+
153+
expect(sendEnvelopeSpy).toHaveBeenCalledTimes(0);
154+
});
155+
});
156+
});

0 commit comments

Comments
 (0)