@sentry/core
Version:
Base implementation for all Sentry JavaScript SDKs
710 lines (622 loc) • 22.1 kB
JavaScript
Object.defineProperty(exports, '__esModule', { value: true });
var hub = require('@sentry/hub');
var utils = require('@sentry/utils');
var api = require('./api.js');
var envelope = require('./envelope.js');
var integration = require('./integration.js');
var ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured.";
/**
* Base implementation for all JavaScript SDK clients.
*
* Call the constructor with the corresponding options
* specific to the client subclass. To access these options later, use
* {@link Client.getOptions}.
*
* If a Dsn is specified in the options, it will be parsed and stored. Use
* {@link Client.getDsn} to retrieve the Dsn at any moment. In case the Dsn is
* invalid, the constructor will throw a {@link SentryException}. Note that
* without a valid Dsn, the SDK will not send any events to Sentry.
*
* Before sending an event, it is passed through
* {@link BaseClient._prepareEvent} to add SDK information and scope data
* (breadcrumbs and context). To add more custom information, override this
* method and extend the resulting prepared event.
*
* To issue automatically created events (e.g. via instrumentation), use
* {@link Client.captureEvent}. It will prepare the event and pass it through
* the callback lifecycle. To issue auto-breadcrumbs, use
* {@link Client.addBreadcrumb}.
*
* @example
* class NodeClient extends BaseClient<NodeOptions> {
* public constructor(options: NodeOptions) {
* super(options);
* }
*
* // ...
* }
*/
class BaseClient {
/** Options passed to the SDK. */
/** The client Dsn, if specified in options. Without this Dsn, the SDK will be disabled. */
/** Array of set up integrations. */
__init() {this._integrations = {};}
/** Indicates whether this client's integrations have been set up. */
__init2() {this._integrationsInitialized = false;}
/** Number of calls being processed */
__init3() {this._numProcessing = 0;}
/** Holds flushable */
__init4() {this._outcomes = {};}
/**
* Initializes this client instance.
*
* @param options Options for the client.
*/
constructor(options) {;BaseClient.prototype.__init.call(this);BaseClient.prototype.__init2.call(this);BaseClient.prototype.__init3.call(this);BaseClient.prototype.__init4.call(this);
this._options = options;
if (options.dsn) {
this._dsn = utils.makeDsn(options.dsn);
var url = api.getEnvelopeEndpointWithUrlEncodedAuth(this._dsn, options);
this._transport = options.transport({
recordDroppedEvent: this.recordDroppedEvent.bind(this),
...options.transportOptions,
url,
});
} else {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.warn('No DSN provided, client will not do anything.');
}
}
/**
* @inheritDoc
*/
captureException(exception, hint, scope) {
// ensure we haven't captured this very object before
if (utils.checkOrSetAlreadyCaught(exception)) {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.log(ALREADY_SEEN_ERROR);
return;
}
let eventId = hint && hint.event_id;
this._process(
this.eventFromException(exception, hint)
.then(event => this._captureEvent(event, hint, scope))
.then(result => {
eventId = result;
}),
);
return eventId;
}
/**
* @inheritDoc
*/
captureMessage(
message,
level,
hint,
scope,
) {
let eventId = hint && hint.event_id;
var promisedEvent = utils.isPrimitive(message)
? this.eventFromMessage(String(message), level, hint)
: this.eventFromException(message, hint);
this._process(
promisedEvent
.then(event => this._captureEvent(event, hint, scope))
.then(result => {
eventId = result;
}),
);
return eventId;
}
/**
* @inheritDoc
*/
captureEvent(event, hint, scope) {
// ensure we haven't captured this very object before
if (hint && hint.originalException && utils.checkOrSetAlreadyCaught(hint.originalException)) {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.log(ALREADY_SEEN_ERROR);
return;
}
let eventId = hint && hint.event_id;
this._process(
this._captureEvent(event, hint, scope).then(result => {
eventId = result;
}),
);
return eventId;
}
/**
* @inheritDoc
*/
captureSession(session) {
if (!this._isEnabled()) {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.warn('SDK not enabled, will not capture session.');
return;
}
if (!(typeof session.release === 'string')) {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.warn('Discarded session because of missing or non-string release');
} else {
this.sendSession(session);
// After sending, we set init false to indicate it's not the first occurrence
hub.updateSession(session, { init: false });
}
}
/**
* @inheritDoc
*/
getDsn() {
return this._dsn;
}
/**
* @inheritDoc
*/
getOptions() {
return this._options;
}
/**
* @inheritDoc
*/
getTransport() {
return this._transport;
}
/**
* @inheritDoc
*/
flush(timeout) {
var transport = this._transport;
if (transport) {
return this._isClientDoneProcessing(timeout).then(clientFinished => {
return transport.flush(timeout).then(transportFlushed => clientFinished && transportFlushed);
});
} else {
return utils.resolvedSyncPromise(true);
}
}
/**
* @inheritDoc
*/
close(timeout) {
return this.flush(timeout).then(result => {
this.getOptions().enabled = false;
return result;
});
}
/**
* Sets up the integrations
*/
setupIntegrations() {
if (this._isEnabled() && !this._integrationsInitialized) {
this._integrations = integration.setupIntegrations(this._options.integrations);
this._integrationsInitialized = true;
}
}
/**
* Gets an installed integration by its `id`.
*
* @returns The installed integration or `undefined` if no integration with that `id` was installed.
*/
getIntegrationById(integrationId) {
return this._integrations[integrationId];
}
/**
* @inheritDoc
*/
getIntegration(integration) {
try {
return (this._integrations[integration.id] ) || null;
} catch (_oO) {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.warn(`Cannot retrieve integration ${integration.id} from the current Client`);
return null;
}
}
/**
* @inheritDoc
*/
sendEvent(event, hint = {}) {
if (this._dsn) {
let env = envelope.createEventEnvelope(event, this._dsn, this._options._metadata, this._options.tunnel);
for (var attachment of hint.attachments || []) {
env = utils.addItemToEnvelope(
env,
utils.createAttachmentEnvelopeItem(
attachment,
this._options.transportOptions && this._options.transportOptions.textEncoder,
),
);
}
this._sendEnvelope(env);
}
}
/**
* @inheritDoc
*/
sendSession(session) {
if (this._dsn) {
var env = envelope.createSessionEnvelope(session, this._dsn, this._options._metadata, this._options.tunnel);
this._sendEnvelope(env);
}
}
/**
* @inheritDoc
*/
recordDroppedEvent(reason, category) {
if (this._options.sendClientReports) {
// We want to track each category (error, transaction, session) separately
// but still keep the distinction between different type of outcomes.
// We could use nested maps, but it's much easier to read and type this way.
// A correct type for map-based implementation if we want to go that route
// would be `Partial<Record<SentryRequestType, Partial<Record<Outcome, number>>>>`
// With typescript 4.1 we could even use template literal types
var key = `${reason}:${category}`;
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.log(`Adding outcome: "${key}"`);
// The following works because undefined + 1 === NaN and NaN is falsy
this._outcomes[key] = this._outcomes[key] + 1 || 1;
}
}
/** Updates existing session based on the provided event */
_updateSessionFromEvent(session, event) {
let crashed = false;
let errored = false;
var exceptions = event.exception && event.exception.values;
if (exceptions) {
errored = true;
for (var ex of exceptions) {
var mechanism = ex.mechanism;
if (mechanism && mechanism.handled === false) {
crashed = true;
break;
}
}
}
// A session is updated and that session update is sent in only one of the two following scenarios:
// 1. Session with non terminal status and 0 errors + an error occurred -> Will set error count to 1 and send update
// 2. Session with non terminal status and 1 error + a crash occurred -> Will set status crashed and send update
var sessionNonTerminal = session.status === 'ok';
var shouldUpdateAndSend = (sessionNonTerminal && session.errors === 0) || (sessionNonTerminal && crashed);
if (shouldUpdateAndSend) {
hub.updateSession(session, {
...(crashed && { status: 'crashed' }),
errors: session.errors || Number(errored || crashed),
});
this.captureSession(session);
}
}
/**
* Determine if the client is finished processing. Returns a promise because it will wait `timeout` ms before saying
* "no" (resolving to `false`) in order to give the client a chance to potentially finish first.
*
* @param timeout The time, in ms, after which to resolve to `false` if the client is still busy. Passing `0` (or not
* passing anything) will make the promise wait as long as it takes for processing to finish before resolving to
* `true`.
* @returns A promise which will resolve to `true` if processing is already done or finishes before the timeout, and
* `false` otherwise
*/
_isClientDoneProcessing(timeout) {
return new utils.SyncPromise(resolve => {
let ticked = 0;
var tick = 1;
var interval = setInterval(() => {
if (this._numProcessing == 0) {
clearInterval(interval);
resolve(true);
} else {
ticked += tick;
if (timeout && ticked >= timeout) {
clearInterval(interval);
resolve(false);
}
}
}, tick);
});
}
/** Determines whether this SDK is enabled and a valid Dsn is present. */
_isEnabled() {
return this.getOptions().enabled !== false && this._dsn !== undefined;
}
/**
* Adds common information to events.
*
* The information includes release and environment from `options`,
* breadcrumbs and context (extra, tags and user) from the scope.
*
* Information that is already present in the event is never overwritten. For
* nested objects, such as the context, keys are merged.
*
* @param event The original event.
* @param hint May contain additional information about the original exception.
* @param scope A scope containing event metadata.
* @returns A new event with more information.
*/
_prepareEvent(event, hint, scope) {
const { normalizeDepth = 3, normalizeMaxBreadth = 1000 } = this.getOptions();
var prepared = {
...event,
event_id: event.event_id || hint.event_id || utils.uuid4(),
timestamp: event.timestamp || utils.dateTimestampInSeconds(),
};
this._applyClientOptions(prepared);
this._applyIntegrationsMetadata(prepared);
// If we have scope given to us, use it as the base for further modifications.
// This allows us to prevent unnecessary copying of data if `captureContext` is not provided.
let finalScope = scope;
if (hint.captureContext) {
finalScope = hub.Scope.clone(finalScope).update(hint.captureContext);
}
// We prepare the result here with a resolved Event.
let result = utils.resolvedSyncPromise(prepared);
// This should be the last thing called, since we want that
// {@link Hub.addEventProcessor} gets the finished prepared event.
if (finalScope) {
// Collect attachments from the hint and scope
var attachments = [...(hint.attachments || []), ...finalScope.getAttachments()];
if (attachments.length) {
hint.attachments = attachments;
}
// In case we have a hub we reassign it.
result = finalScope.applyToEvent(prepared, hint);
}
return result.then(evt => {
if (typeof normalizeDepth === 'number' && normalizeDepth > 0) {
return this._normalizeEvent(evt, normalizeDepth, normalizeMaxBreadth);
}
return evt;
});
}
/**
* Applies `normalize` function on necessary `Event` attributes to make them safe for serialization.
* Normalized keys:
* - `breadcrumbs.data`
* - `user`
* - `contexts`
* - `extra`
* @param event Event
* @returns Normalized event
*/
_normalizeEvent(event, depth, maxBreadth) {
if (!event) {
return null;
}
var normalized = {
...event,
...(event.breadcrumbs && {
breadcrumbs: event.breadcrumbs.map(b => ({
...b,
...(b.data && {
data: utils.normalize(b.data, depth, maxBreadth),
}),
})),
}),
...(event.user && {
user: utils.normalize(event.user, depth, maxBreadth),
}),
...(event.contexts && {
contexts: utils.normalize(event.contexts, depth, maxBreadth),
}),
...(event.extra && {
extra: utils.normalize(event.extra, depth, maxBreadth),
}),
};
// event.contexts.trace stores information about a Transaction. Similarly,
// event.spans[] stores information about child Spans. Given that a
// Transaction is conceptually a Span, normalization should apply to both
// Transactions and Spans consistently.
// For now the decision is to skip normalization of Transactions and Spans,
// so this block overwrites the normalized event to add back the original
// Transaction information prior to normalization.
if (event.contexts && event.contexts.trace && normalized.contexts) {
normalized.contexts.trace = event.contexts.trace;
// event.contexts.trace.data may contain circular/dangerous data so we need to normalize it
if (event.contexts.trace.data) {
normalized.contexts.trace.data = utils.normalize(event.contexts.trace.data, depth, maxBreadth);
}
}
// event.spans[].data may contain circular/dangerous data so we need to normalize it
if (event.spans) {
normalized.spans = event.spans.map(span => {
// We cannot use the spread operator here because `toJSON` on `span` is non-enumerable
if (span.data) {
span.data = utils.normalize(span.data, depth, maxBreadth);
}
return span;
});
}
return normalized;
}
/**
* Enhances event using the client configuration.
* It takes care of all "static" values like environment, release and `dist`,
* as well as truncating overly long values.
* @param event event instance to be enhanced
*/
_applyClientOptions(event) {
var options = this.getOptions();
const { environment, release, dist, maxValueLength = 250 } = options;
if (!('environment' in event)) {
event.environment = 'environment' in options ? environment : 'production';
}
if (event.release === undefined && release !== undefined) {
event.release = release;
}
if (event.dist === undefined && dist !== undefined) {
event.dist = dist;
}
if (event.message) {
event.message = utils.truncate(event.message, maxValueLength);
}
var exception = event.exception && event.exception.values && event.exception.values[0];
if (exception && exception.value) {
exception.value = utils.truncate(exception.value, maxValueLength);
}
var request = event.request;
if (request && request.url) {
request.url = utils.truncate(request.url, maxValueLength);
}
}
/**
* This function adds all used integrations to the SDK info in the event.
* @param event The event that will be filled with all integrations.
*/
_applyIntegrationsMetadata(event) {
var integrationsArray = Object.keys(this._integrations);
if (integrationsArray.length > 0) {
event.sdk = event.sdk || {};
event.sdk.integrations = [...(event.sdk.integrations || []), ...integrationsArray];
}
}
/**
* Processes the event and logs an error in case of rejection
* @param event
* @param hint
* @param scope
*/
_captureEvent(event, hint = {}, scope) {
return this._processEvent(event, hint, scope).then(
finalEvent => {
return finalEvent.event_id;
},
reason => {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.warn(reason);
return undefined;
},
);
}
/**
* Processes an event (either error or message) and sends it to Sentry.
*
* This also adds breadcrumbs and context information to the event. However,
* platform specific meta data (such as the User's IP address) must be added
* by the SDK implementor.
*
*
* @param event The event to send to Sentry.
* @param hint May contain additional information about the original exception.
* @param scope A scope containing event metadata.
* @returns A SyncPromise that resolves with the event or rejects in case event was/will not be send.
*/
_processEvent(event, hint, scope) {
const { beforeSend, sampleRate } = this.getOptions();
if (!this._isEnabled()) {
return utils.rejectedSyncPromise(new utils.SentryError('SDK not enabled, will not capture event.'));
}
var isTransaction = event.type === 'transaction';
// 1.0 === 100% events are sent
// 0.0 === 0% events are sent
// Sampling for transaction happens somewhere else
if (!isTransaction && typeof sampleRate === 'number' && Math.random() > sampleRate) {
this.recordDroppedEvent('sample_rate', 'error');
return utils.rejectedSyncPromise(
new utils.SentryError(
`Discarding event because it's not included in the random sample (sampling rate = ${sampleRate})`,
),
);
}
return this._prepareEvent(event, hint, scope)
.then(prepared => {
if (prepared === null) {
this.recordDroppedEvent('event_processor', event.type || 'error');
throw new utils.SentryError('An event processor returned null, will not send event.');
}
var isInternalException = hint.data && (hint.data ).__sentry__ === true;
if (isInternalException || isTransaction || !beforeSend) {
return prepared;
}
var beforeSendResult = beforeSend(prepared, hint);
return _ensureBeforeSendRv(beforeSendResult);
})
.then(processedEvent => {
if (processedEvent === null) {
this.recordDroppedEvent('before_send', event.type || 'error');
throw new utils.SentryError('`beforeSend` returned `null`, will not send event.');
}
var session = scope && scope.getSession();
if (!isTransaction && session) {
this._updateSessionFromEvent(session, processedEvent);
}
this.sendEvent(processedEvent, hint);
return processedEvent;
})
.then(null, reason => {
if (reason instanceof utils.SentryError) {
throw reason;
}
this.captureException(reason, {
data: {
__sentry__: true,
},
originalException: reason ,
});
throw new utils.SentryError(
`Event processing pipeline threw an error, original event will not be sent. Details have been sent as a new event.\nReason: ${reason}`,
);
});
}
/**
* Occupies the client with processing and event
*/
_process(promise) {
this._numProcessing += 1;
void promise.then(
value => {
this._numProcessing -= 1;
return value;
},
reason => {
this._numProcessing -= 1;
return reason;
},
);
}
/**
* @inheritdoc
*/
_sendEnvelope(envelope) {
if (this._transport && this._dsn) {
this._transport.send(envelope).then(null, reason => {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.error('Error while sending event:', reason);
});
} else {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.error('Transport disabled');
}
}
/**
* Clears outcomes on this client and returns them.
*/
_clearOutcomes() {
var outcomes = this._outcomes;
this._outcomes = {};
return Object.keys(outcomes).map(key => {
const [reason, category] = key.split(':') ;
return {
reason,
category,
quantity: outcomes[key],
};
});
}
/**
* @inheritDoc
*/
}
/**
* Verifies that return value of configured `beforeSend` is of expected type.
*/
function _ensureBeforeSendRv(rv) {
var nullErr = '`beforeSend` method has to return `null` or a valid event.';
if (utils.isThenable(rv)) {
return rv.then(
event => {
if (!(utils.isPlainObject(event) || event === null)) {
throw new utils.SentryError(nullErr);
}
return event;
},
e => {
throw new utils.SentryError(`beforeSend rejected with ${e}`);
},
);
} else if (!(utils.isPlainObject(rv) || rv === null)) {
throw new utils.SentryError(nullErr);
}
return rv;
}
exports.BaseClient = BaseClient;
//# sourceMappingURL=baseclient.js.map