UNPKG

@sentry/core

Version:
710 lines (622 loc) 22.1 kB
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