Skip to content

Commit e313acd

Browse files
committed
feat(core): Add ServerRuntimeClient
1 parent 61450cb commit e313acd

File tree

3 files changed

+302
-0
lines changed

3 files changed

+302
-0
lines changed

packages/core/src/eventbuilder.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import type {
2+
Event,
3+
EventHint,
4+
Exception,
5+
Mechanism,
6+
Severity,
7+
SeverityLevel,
8+
StackFrame,
9+
StackParser,
10+
} from '@sentry/types';
11+
import {
12+
addExceptionMechanism,
13+
addExceptionTypeValue,
14+
extractExceptionKeysForMessage,
15+
isError,
16+
isPlainObject,
17+
normalizeToSize,
18+
} from '@sentry/utils';
19+
20+
import { getCurrentHub } from './hub';
21+
22+
/**
23+
* Extracts stack frames from the error.stack string
24+
*/
25+
export function parseStackFrames(stackParser: StackParser, error: Error): StackFrame[] {
26+
return stackParser(error.stack || '', 1);
27+
}
28+
29+
/**
30+
* Extracts stack frames from the error and builds a Sentry Exception
31+
*/
32+
export function exceptionFromError(stackParser: StackParser, error: Error): Exception {
33+
const exception: Exception = {
34+
type: error.name || error.constructor.name,
35+
value: error.message,
36+
};
37+
38+
const frames = parseStackFrames(stackParser, error);
39+
if (frames.length) {
40+
exception.stacktrace = { frames };
41+
}
42+
43+
return exception;
44+
}
45+
46+
/**
47+
* Builds and Event from a Exception
48+
* @hidden
49+
*/
50+
export function eventFromUnknownInput(stackParser: StackParser, exception: unknown, hint?: EventHint): Event {
51+
let ex: unknown = exception;
52+
const providedMechanism: Mechanism | undefined =
53+
hint && hint.data && (hint.data as { mechanism: Mechanism }).mechanism;
54+
const mechanism: Mechanism = providedMechanism || {
55+
handled: true,
56+
type: 'generic',
57+
};
58+
59+
if (!isError(exception)) {
60+
if (isPlainObject(exception)) {
61+
// This will allow us to group events based on top-level keys
62+
// which is much better than creating new group when any key/value change
63+
const message = `Non-Error exception captured with keys: ${extractExceptionKeysForMessage(exception)}`;
64+
65+
const hub = getCurrentHub();
66+
const client = hub.getClient();
67+
const normalizeDepth = client && client.getOptions().normalizeDepth;
68+
hub.configureScope(scope => {
69+
scope.setExtra('__serialized__', normalizeToSize(exception, normalizeDepth));
70+
});
71+
72+
ex = (hint && hint.syntheticException) || new Error(message);
73+
(ex as Error).message = message;
74+
} else {
75+
// This handles when someone does: `throw "something awesome";`
76+
// We use synthesized Error here so we can extract a (rough) stack trace.
77+
ex = (hint && hint.syntheticException) || new Error(exception as string);
78+
(ex as Error).message = exception as string;
79+
}
80+
mechanism.synthetic = true;
81+
}
82+
83+
const event = {
84+
exception: {
85+
values: [exceptionFromError(stackParser, ex as Error)],
86+
},
87+
};
88+
89+
addExceptionTypeValue(event, undefined, undefined);
90+
addExceptionMechanism(event, mechanism);
91+
92+
return {
93+
...event,
94+
event_id: hint && hint.event_id,
95+
};
96+
}
97+
98+
/**
99+
* Builds and Event from a Message
100+
* @hidden
101+
*/
102+
export function eventFromMessage(
103+
stackParser: StackParser,
104+
message: string,
105+
// eslint-disable-next-line deprecation/deprecation
106+
level: Severity | SeverityLevel = 'info',
107+
hint?: EventHint,
108+
attachStacktrace?: boolean,
109+
): Event {
110+
const event: Event = {
111+
event_id: hint && hint.event_id,
112+
level,
113+
message,
114+
};
115+
116+
if (attachStacktrace && hint && hint.syntheticException) {
117+
const frames = parseStackFrames(stackParser, hint.syntheticException);
118+
if (frames.length) {
119+
event.exception = {
120+
values: [
121+
{
122+
value: message,
123+
stacktrace: { frames },
124+
},
125+
],
126+
};
127+
}
128+
}
129+
130+
return event;
131+
}

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export { SessionFlusher } from './sessionflusher';
3838
export { addGlobalEventProcessor, Scope } from './scope';
3939
export { getEnvelopeEndpointWithUrlEncodedAuth, getReportDialogEndpoint } from './api';
4040
export { BaseClient } from './baseclient';
41+
export { ServerRuntimeClient } from './server-runtime-client';
4142
export { initAndBind } from './sdk';
4243
export { createTransport } from './transports/base';
4344
export { makeOfflineTransport } from './transports/offline';
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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 { logger, uuid4 } from '@sentry/utils';
15+
16+
import { BaseClient } from './baseclient';
17+
import { createCheckInEnvelope } from './checkin';
18+
import { eventFromMessage, eventFromUnknownInput } from './eventbuilder';
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 extends BaseClient<ServerRuntimeClientOptions> {
32+
/**
33+
* Creates a new Edge SDK instance.
34+
* @param options Configuration options for this SDK.
35+
*/
36+
public constructor(options: ServerRuntimeClientOptions) {
37+
// Server clients always support tracing
38+
addTracingExtensions();
39+
40+
super(options);
41+
}
42+
43+
/**
44+
* @inheritDoc
45+
*/
46+
public eventFromException(exception: unknown, hint?: EventHint): PromiseLike<Event> {
47+
return Promise.resolve(eventFromUnknownInput(this._options.stackParser, exception, hint));
48+
}
49+
50+
/**
51+
* @inheritDoc
52+
*/
53+
public eventFromMessage(
54+
message: string,
55+
// eslint-disable-next-line deprecation/deprecation
56+
level: Severity | SeverityLevel = 'info',
57+
hint?: EventHint,
58+
): PromiseLike<Event> {
59+
return Promise.resolve(
60+
eventFromMessage(this._options.stackParser, message, level, hint, this._options.attachStacktrace),
61+
);
62+
}
63+
64+
/**
65+
* Create a cron monitor check in and send it to Sentry.
66+
*
67+
* @param checkIn An object that describes a check in.
68+
* @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want
69+
* to create a monitor automatically when sending a check in.
70+
*/
71+
public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig, scope?: Scope): string {
72+
const id = checkIn.status !== 'in_progress' && checkIn.checkInId ? checkIn.checkInId : uuid4();
73+
if (!this._isEnabled()) {
74+
__DEBUG_BUILD__ && logger.warn('SDK not enabled, will not capture checkin.');
75+
return id;
76+
}
77+
78+
const options = this.getOptions();
79+
const { release, environment, tunnel } = options;
80+
81+
const serializedCheckIn: SerializedCheckIn = {
82+
check_in_id: id,
83+
monitor_slug: checkIn.monitorSlug,
84+
status: checkIn.status,
85+
release,
86+
environment,
87+
};
88+
89+
if (checkIn.status !== 'in_progress') {
90+
serializedCheckIn.duration = checkIn.duration;
91+
}
92+
93+
if (monitorConfig) {
94+
serializedCheckIn.monitor_config = {
95+
schedule: monitorConfig.schedule,
96+
checkin_margin: monitorConfig.checkinMargin,
97+
max_runtime: monitorConfig.maxRuntime,
98+
timezone: monitorConfig.timezone,
99+
};
100+
}
101+
102+
const [dynamicSamplingContext, traceContext] = this._getTraceInfoFromScope(scope);
103+
if (traceContext) {
104+
serializedCheckIn.contexts = {
105+
trace: traceContext,
106+
};
107+
}
108+
109+
const envelope = createCheckInEnvelope(
110+
serializedCheckIn,
111+
dynamicSamplingContext,
112+
this.getSdkMetadata(),
113+
tunnel,
114+
this.getDsn(),
115+
);
116+
117+
__DEBUG_BUILD__ && logger.info('Sending checkin:', checkIn.monitorSlug, checkIn.status);
118+
void this._sendEnvelope(envelope);
119+
return id;
120+
}
121+
122+
/**
123+
* @inheritDoc
124+
*/
125+
protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike<Event | null> {
126+
if (this._options.platform) {
127+
event.platform = event.platform || this._options.platform;
128+
}
129+
130+
if (this._options.runtime) {
131+
event.contexts = {
132+
...event.contexts,
133+
runtime: (event.contexts || {}).runtime || this._options.runtime,
134+
};
135+
}
136+
137+
if (this._options.serverName) {
138+
event.server_name = event.server_name || this._options.serverName;
139+
}
140+
141+
return super._prepareEvent(event, hint, scope);
142+
}
143+
144+
/** Extract trace information from scope */
145+
private _getTraceInfoFromScope(
146+
scope: Scope | undefined,
147+
): [dynamicSamplingContext: Partial<DynamicSamplingContext> | undefined, traceContext: TraceContext | undefined] {
148+
if (!scope) {
149+
return [undefined, undefined];
150+
}
151+
152+
const span = scope.getSpan();
153+
if (span) {
154+
const samplingContext = span.transaction ? span.transaction.getDynamicSamplingContext() : undefined;
155+
return [samplingContext, span.getTraceContext()];
156+
}
157+
158+
const { traceId, spanId, parentSpanId, dsc } = scope.getPropagationContext();
159+
const traceContext: TraceContext = {
160+
trace_id: traceId,
161+
span_id: spanId,
162+
parent_span_id: parentSpanId,
163+
};
164+
if (dsc) {
165+
return [dsc, traceContext];
166+
}
167+
168+
return [getDynamicSamplingContextFromClient(traceId, this, scope), traceContext];
169+
}
170+
}

0 commit comments

Comments
 (0)