-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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`.
- Loading branch information
Showing
5 changed files
with
463 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
import type { | ||
BaseTransportOptions, | ||
CheckIn, | ||
ClientOptions, | ||
DynamicSamplingContext, | ||
Event, | ||
EventHint, | ||
MonitorConfig, | ||
SerializedCheckIn, | ||
Severity, | ||
SeverityLevel, | ||
TraceContext, | ||
} from '@sentry/types'; | ||
import { eventFromMessage, eventFromUnknownInput, logger, uuid4 } from '@sentry/utils'; | ||
|
||
import { BaseClient } from './baseclient'; | ||
import { createCheckInEnvelope } from './checkin'; | ||
import { getCurrentHub } from './hub'; | ||
import type { Scope } from './scope'; | ||
import { addTracingExtensions, getDynamicSamplingContextFromClient } from './tracing'; | ||
|
||
export interface ServerRuntimeClientOptions extends ClientOptions<BaseTransportOptions> { | ||
platform?: string; | ||
runtime?: { name: string; version?: string }; | ||
serverName?: string; | ||
} | ||
|
||
/** | ||
* The Sentry Server Runtime Client SDK. | ||
*/ | ||
export class ServerRuntimeClient< | ||
O extends ClientOptions & ServerRuntimeClientOptions = ServerRuntimeClientOptions, | ||
> extends BaseClient<O> { | ||
/** | ||
* Creates a new Edge SDK instance. | ||
* @param options Configuration options for this SDK. | ||
*/ | ||
public constructor(options: O) { | ||
// Server clients always support tracing | ||
addTracingExtensions(); | ||
|
||
super(options); | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public eventFromException(exception: unknown, hint?: EventHint): PromiseLike<Event> { | ||
return Promise.resolve(eventFromUnknownInput(getCurrentHub, this._options.stackParser, exception, hint)); | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public eventFromMessage( | ||
message: string, | ||
// eslint-disable-next-line deprecation/deprecation | ||
level: Severity | SeverityLevel = 'info', | ||
hint?: EventHint, | ||
): PromiseLike<Event> { | ||
return Promise.resolve( | ||
eventFromMessage(this._options.stackParser, message, level, hint, this._options.attachStacktrace), | ||
); | ||
} | ||
|
||
/** | ||
* Create a cron monitor check in and send it to Sentry. | ||
* | ||
* @param checkIn An object that describes a check in. | ||
* @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want | ||
* to create a monitor automatically when sending a check in. | ||
*/ | ||
public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig, scope?: Scope): string { | ||
const id = checkIn.status !== 'in_progress' && checkIn.checkInId ? checkIn.checkInId : uuid4(); | ||
if (!this._isEnabled()) { | ||
__DEBUG_BUILD__ && logger.warn('SDK not enabled, will not capture checkin.'); | ||
return id; | ||
} | ||
|
||
const options = this.getOptions(); | ||
const { release, environment, tunnel } = options; | ||
|
||
const serializedCheckIn: SerializedCheckIn = { | ||
check_in_id: id, | ||
monitor_slug: checkIn.monitorSlug, | ||
status: checkIn.status, | ||
release, | ||
environment, | ||
}; | ||
|
||
if (checkIn.status !== 'in_progress') { | ||
serializedCheckIn.duration = checkIn.duration; | ||
} | ||
|
||
if (monitorConfig) { | ||
serializedCheckIn.monitor_config = { | ||
schedule: monitorConfig.schedule, | ||
checkin_margin: monitorConfig.checkinMargin, | ||
max_runtime: monitorConfig.maxRuntime, | ||
timezone: monitorConfig.timezone, | ||
}; | ||
} | ||
|
||
const [dynamicSamplingContext, traceContext] = this._getTraceInfoFromScope(scope); | ||
if (traceContext) { | ||
serializedCheckIn.contexts = { | ||
trace: traceContext, | ||
}; | ||
} | ||
|
||
const envelope = createCheckInEnvelope( | ||
serializedCheckIn, | ||
dynamicSamplingContext, | ||
this.getSdkMetadata(), | ||
tunnel, | ||
this.getDsn(), | ||
); | ||
|
||
__DEBUG_BUILD__ && logger.info('Sending checkin:', checkIn.monitorSlug, checkIn.status); | ||
void this._sendEnvelope(envelope); | ||
return id; | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike<Event | null> { | ||
if (this._options.platform) { | ||
event.platform = event.platform || this._options.platform; | ||
} | ||
|
||
if (this._options.runtime) { | ||
event.contexts = { | ||
...event.contexts, | ||
runtime: (event.contexts || {}).runtime || this._options.runtime, | ||
}; | ||
} | ||
|
||
if (this._options.serverName) { | ||
event.server_name = event.server_name || this._options.serverName; | ||
} | ||
|
||
return super._prepareEvent(event, hint, scope); | ||
} | ||
|
||
/** Extract trace information from scope */ | ||
private _getTraceInfoFromScope( | ||
scope: Scope | undefined, | ||
): [dynamicSamplingContext: Partial<DynamicSamplingContext> | undefined, traceContext: TraceContext | undefined] { | ||
if (!scope) { | ||
return [undefined, undefined]; | ||
} | ||
|
||
const span = scope.getSpan(); | ||
if (span) { | ||
const samplingContext = span.transaction ? span.transaction.getDynamicSamplingContext() : undefined; | ||
return [samplingContext, span.getTraceContext()]; | ||
} | ||
|
||
const { traceId, spanId, parentSpanId, dsc } = scope.getPropagationContext(); | ||
const traceContext: TraceContext = { | ||
trace_id: traceId, | ||
span_id: spanId, | ||
parent_span_id: parentSpanId, | ||
}; | ||
if (dsc) { | ||
return [dsc, traceContext]; | ||
} | ||
|
||
return [getDynamicSamplingContextFromClient(traceId, this, scope), traceContext]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import type { Event, EventHint } from '@sentry/types'; | ||
|
||
import { createTransport } from '../../src'; | ||
import type { ServerRuntimeClientOptions } from '../../src/server-runtime-client'; | ||
import { ServerRuntimeClient } from '../../src/server-runtime-client'; | ||
|
||
const PUBLIC_DSN = 'https://username@domain/123'; | ||
|
||
function getDefaultClientOptions(options: Partial<ServerRuntimeClientOptions> = {}): ServerRuntimeClientOptions { | ||
return { | ||
integrations: [], | ||
transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})), | ||
stackParser: () => [], | ||
instrumenter: 'sentry', | ||
...options, | ||
}; | ||
} | ||
|
||
describe('ServerRuntimeClient', () => { | ||
let client: ServerRuntimeClient; | ||
|
||
describe('_prepareEvent', () => { | ||
test('adds platform to event', () => { | ||
const options = getDefaultClientOptions({ dsn: PUBLIC_DSN }); | ||
const client = new ServerRuntimeClient({ ...options, platform: 'edge' }); | ||
|
||
const event: Event = {}; | ||
const hint: EventHint = {}; | ||
(client as any)._prepareEvent(event, hint); | ||
|
||
expect(event.platform).toEqual('edge'); | ||
}); | ||
|
||
test('adds server_name to event', () => { | ||
const options = getDefaultClientOptions({ dsn: PUBLIC_DSN }); | ||
const client = new ServerRuntimeClient({ ...options, serverName: 'server' }); | ||
|
||
const event: Event = {}; | ||
const hint: EventHint = {}; | ||
(client as any)._prepareEvent(event, hint); | ||
|
||
expect(event.server_name).toEqual('server'); | ||
}); | ||
|
||
test('adds runtime context to event', () => { | ||
const options = getDefaultClientOptions({ dsn: PUBLIC_DSN }); | ||
const client = new ServerRuntimeClient({ ...options, runtime: { name: 'edge' } }); | ||
|
||
const event: Event = {}; | ||
const hint: EventHint = {}; | ||
(client as any)._prepareEvent(event, hint); | ||
|
||
expect(event.contexts?.runtime).toEqual({ | ||
name: 'edge', | ||
}); | ||
}); | ||
|
||
test("doesn't clobber existing runtime data", () => { | ||
const options = getDefaultClientOptions({ dsn: PUBLIC_DSN }); | ||
const client = new ServerRuntimeClient({ ...options, runtime: { name: 'edge' } }); | ||
|
||
const event: Event = { contexts: { runtime: { name: 'foo', version: '1.2.3' } } }; | ||
const hint: EventHint = {}; | ||
(client as any)._prepareEvent(event, hint); | ||
|
||
expect(event.contexts?.runtime).toEqual({ name: 'foo', version: '1.2.3' }); | ||
expect(event.contexts?.runtime).not.toEqual({ name: 'edge' }); | ||
}); | ||
}); | ||
|
||
describe('captureCheckIn', () => { | ||
it('sends a checkIn envelope', () => { | ||
const options = getDefaultClientOptions({ | ||
dsn: PUBLIC_DSN, | ||
serverName: 'bar', | ||
release: '1.0.0', | ||
environment: 'dev', | ||
}); | ||
client = new ServerRuntimeClient(options); | ||
|
||
// @ts-ignore accessing private method | ||
const sendEnvelopeSpy = jest.spyOn(client, '_sendEnvelope'); | ||
|
||
const id = client.captureCheckIn( | ||
{ monitorSlug: 'foo', status: 'in_progress' }, | ||
{ | ||
schedule: { | ||
type: 'crontab', | ||
value: '0 * * * *', | ||
}, | ||
checkinMargin: 2, | ||
maxRuntime: 12333, | ||
timezone: 'Canada/Eastern', | ||
}, | ||
); | ||
|
||
expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); | ||
expect(sendEnvelopeSpy).toHaveBeenCalledWith([ | ||
expect.any(Object), | ||
[ | ||
[ | ||
expect.any(Object), | ||
{ | ||
check_in_id: id, | ||
monitor_slug: 'foo', | ||
status: 'in_progress', | ||
release: '1.0.0', | ||
environment: 'dev', | ||
monitor_config: { | ||
schedule: { | ||
type: 'crontab', | ||
value: '0 * * * *', | ||
}, | ||
checkin_margin: 2, | ||
max_runtime: 12333, | ||
timezone: 'Canada/Eastern', | ||
}, | ||
}, | ||
], | ||
], | ||
]); | ||
|
||
client.captureCheckIn({ monitorSlug: 'foo', status: 'ok', duration: 1222, checkInId: id }); | ||
|
||
expect(sendEnvelopeSpy).toHaveBeenCalledTimes(2); | ||
expect(sendEnvelopeSpy).toHaveBeenCalledWith([ | ||
expect.any(Object), | ||
[ | ||
[ | ||
expect.any(Object), | ||
{ | ||
check_in_id: id, | ||
monitor_slug: 'foo', | ||
duration: 1222, | ||
status: 'ok', | ||
release: '1.0.0', | ||
environment: 'dev', | ||
}, | ||
], | ||
], | ||
]); | ||
}); | ||
|
||
it('does not send a checkIn envelope if disabled', () => { | ||
const options = getDefaultClientOptions({ dsn: PUBLIC_DSN, serverName: 'bar', enabled: false }); | ||
client = new ServerRuntimeClient(options); | ||
|
||
// @ts-ignore accessing private method | ||
const sendEnvelopeSpy = jest.spyOn(client, '_sendEnvelope'); | ||
|
||
client.captureCheckIn({ monitorSlug: 'foo', status: 'in_progress' }); | ||
|
||
expect(sendEnvelopeSpy).toHaveBeenCalledTimes(0); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.