123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811 |
- Object.defineProperty(exports, '__esModule', { value: true });
- const utils = require('@sentry/utils');
- const api = require('./api.js');
- const debugBuild = require('./debug-build.js');
- const envelope = require('./envelope.js');
- const exports$1 = require('./exports.js');
- const hub = require('./hub.js');
- const integration = require('./integration.js');
- const envelope$1 = require('./metrics/envelope.js');
- const session = require('./session.js');
- const dynamicSamplingContext = require('./tracing/dynamicSamplingContext.js');
- const prepareEvent = require('./utils/prepareEvent.js');
- const 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 {
- /**
- * A reference to a metrics aggregator
- *
- * @experimental Note this is alpha API. It may experience breaking changes in the future.
- */
- /** 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. */
- /** Indicates whether this client's integrations have been set up. */
- /** Number of calls being processed */
- /** Holds flushable */
- // eslint-disable-next-line @typescript-eslint/ban-types
- /**
- * Initializes this client instance.
- *
- * @param options Options for the client.
- */
- constructor(options) {
- this._options = options;
- this._integrations = {};
- this._integrationsInitialized = false;
- this._numProcessing = 0;
- this._outcomes = {};
- this._hooks = {};
- this._eventProcessors = [];
- if (options.dsn) {
- this._dsn = utils.makeDsn(options.dsn);
- } else {
- debugBuild.DEBUG_BUILD && utils.logger.warn('No DSN provided, client will not send events.');
- }
- if (this._dsn) {
- const url = api.getEnvelopeEndpointWithUrlEncodedAuth(this._dsn, options);
- this._transport = options.transport({
- recordDroppedEvent: this.recordDroppedEvent.bind(this),
- ...options.transportOptions,
- url,
- });
- }
- }
- /**
- * @inheritDoc
- */
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
- captureException(exception, hint, scope) {
- // ensure we haven't captured this very object before
- if (utils.checkOrSetAlreadyCaught(exception)) {
- debugBuild.DEBUG_BUILD && 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,
- // eslint-disable-next-line deprecation/deprecation
- level,
- hint,
- scope,
- ) {
- let eventId = hint && hint.event_id;
- const eventMessage = utils.isParameterizedString(message) ? message : String(message);
- const promisedEvent = utils.isPrimitive(message)
- ? this.eventFromMessage(eventMessage, 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)) {
- debugBuild.DEBUG_BUILD && utils.logger.log(ALREADY_SEEN_ERROR);
- return;
- }
- let eventId = hint && hint.event_id;
- const sdkProcessingMetadata = event.sdkProcessingMetadata || {};
- const capturedSpanScope = sdkProcessingMetadata.capturedSpanScope;
- this._process(
- this._captureEvent(event, hint, capturedSpanScope || scope).then(result => {
- eventId = result;
- }),
- );
- return eventId;
- }
- /**
- * @inheritDoc
- */
- captureSession(session$1) {
- if (!(typeof session$1.release === 'string')) {
- debugBuild.DEBUG_BUILD && utils.logger.warn('Discarded session because of missing or non-string release');
- } else {
- this.sendSession(session$1);
- // After sending, we set init false to indicate it's not the first occurrence
- session.updateSession(session$1, { init: false });
- }
- }
- /**
- * @inheritDoc
- */
- getDsn() {
- return this._dsn;
- }
- /**
- * @inheritDoc
- */
- getOptions() {
- return this._options;
- }
- /**
- * @see SdkMetadata in @sentry/types
- *
- * @return The metadata of the SDK
- */
- getSdkMetadata() {
- return this._options._metadata;
- }
- /**
- * @inheritDoc
- */
- getTransport() {
- return this._transport;
- }
- /**
- * @inheritDoc
- */
- flush(timeout) {
- const transport = this._transport;
- if (transport) {
- if (this.metricsAggregator) {
- this.metricsAggregator.flush();
- }
- 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;
- if (this.metricsAggregator) {
- this.metricsAggregator.close();
- }
- return result;
- });
- }
- /** Get all installed event processors. */
- getEventProcessors() {
- return this._eventProcessors;
- }
- /** @inheritDoc */
- addEventProcessor(eventProcessor) {
- this._eventProcessors.push(eventProcessor);
- }
- /**
- * This is an internal function to setup all integrations that should run on the client.
- * @deprecated Use `client.init()` instead.
- */
- setupIntegrations(forceInitialize) {
- if ((forceInitialize && !this._integrationsInitialized) || (this._isEnabled() && !this._integrationsInitialized)) {
- this._setupIntegrations();
- }
- }
- /** @inheritdoc */
- init() {
- if (this._isEnabled()) {
- this._setupIntegrations();
- }
- }
- /**
- * Gets an installed integration by its `id`.
- *
- * @returns The installed integration or `undefined` if no integration with that `id` was installed.
- * @deprecated Use `getIntegrationByName()` instead.
- */
- getIntegrationById(integrationId) {
- return this.getIntegrationByName(integrationId);
- }
- /**
- * Gets an installed integration by its name.
- *
- * @returns The installed integration or `undefined` if no integration with that `name` was installed.
- */
- getIntegrationByName(integrationName) {
- return this._integrations[integrationName] ;
- }
- /**
- * Returns the client's instance of the given integration class, it any.
- * @deprecated Use `getIntegrationByName()` instead.
- */
- getIntegration(integration) {
- try {
- return (this._integrations[integration.id] ) || null;
- } catch (_oO) {
- debugBuild.DEBUG_BUILD && utils.logger.warn(`Cannot retrieve integration ${integration.id} from the current Client`);
- return null;
- }
- }
- /**
- * @inheritDoc
- */
- addIntegration(integration$1) {
- const isAlreadyInstalled = this._integrations[integration$1.name];
- // This hook takes care of only installing if not already installed
- integration.setupIntegration(this, integration$1, this._integrations);
- // Here we need to check manually to make sure to not run this multiple times
- if (!isAlreadyInstalled) {
- integration.afterSetupIntegrations(this, [integration$1]);
- }
- }
- /**
- * @inheritDoc
- */
- sendEvent(event, hint = {}) {
- this.emit('beforeSendEvent', event, hint);
- let env = envelope.createEventEnvelope(event, this._dsn, this._options._metadata, this._options.tunnel);
- for (const attachment of hint.attachments || []) {
- env = utils.addItemToEnvelope(
- env,
- utils.createAttachmentEnvelopeItem(
- attachment,
- this._options.transportOptions && this._options.transportOptions.textEncoder,
- ),
- );
- }
- const promise = this._sendEnvelope(env);
- if (promise) {
- promise.then(sendResponse => this.emit('afterSendEvent', event, sendResponse), null);
- }
- }
- /**
- * @inheritDoc
- */
- sendSession(session) {
- const env = envelope.createSessionEnvelope(session, this._dsn, this._options._metadata, this._options.tunnel);
- // _sendEnvelope should not throw
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this._sendEnvelope(env);
- }
- /**
- * @inheritDoc
- */
- recordDroppedEvent(reason, category, _event) {
- // Note: we use `event` in replay, where we overwrite this hook.
- if (this._options.sendClientReports) {
- // We want to track each category (error, transaction, session, replay_event) 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
- const key = `${reason}:${category}`;
- debugBuild.DEBUG_BUILD && 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;
- }
- }
- /**
- * @inheritDoc
- */
- captureAggregateMetrics(metricBucketItems) {
- debugBuild.DEBUG_BUILD && utils.logger.log(`Flushing aggregated metrics, number of metrics: ${metricBucketItems.length}`);
- const metricsEnvelope = envelope$1.createMetricEnvelope(
- metricBucketItems,
- this._dsn,
- this._options._metadata,
- this._options.tunnel,
- );
- // _sendEnvelope should not throw
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this._sendEnvelope(metricsEnvelope);
- }
- // Keep on() & emit() signatures in sync with types' client.ts interface
- /* eslint-disable @typescript-eslint/unified-signatures */
- /** @inheritdoc */
- /** @inheritdoc */
- on(hook, callback) {
- if (!this._hooks[hook]) {
- this._hooks[hook] = [];
- }
- // @ts-expect-error We assue the types are correct
- this._hooks[hook].push(callback);
- }
- /** @inheritdoc */
- /** @inheritdoc */
- emit(hook, ...rest) {
- if (this._hooks[hook]) {
- this._hooks[hook].forEach(callback => callback(...rest));
- }
- }
- /* eslint-enable @typescript-eslint/unified-signatures */
- /** Setup integrations for this client. */
- _setupIntegrations() {
- const { integrations } = this._options;
- this._integrations = integration.setupIntegrations(this, integrations);
- integration.afterSetupIntegrations(this, integrations);
- // TODO v8: We don't need this flag anymore
- this._integrationsInitialized = true;
- }
- /** Updates existing session based on the provided event */
- _updateSessionFromEvent(session$1, event) {
- let crashed = false;
- let errored = false;
- const exceptions = event.exception && event.exception.values;
- if (exceptions) {
- errored = true;
- for (const ex of exceptions) {
- const 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
- const sessionNonTerminal = session$1.status === 'ok';
- const shouldUpdateAndSend = (sessionNonTerminal && session$1.errors === 0) || (sessionNonTerminal && crashed);
- if (shouldUpdateAndSend) {
- session.updateSession(session$1, {
- ...(crashed && { status: 'crashed' }),
- errors: session$1.errors || Number(errored || crashed),
- });
- this.captureSession(session$1);
- }
- }
- /**
- * 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;
- const tick = 1;
- const 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 transport is present. */
- _isEnabled() {
- return this.getOptions().enabled !== false && this._transport !== 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,
- isolationScope = hub.getIsolationScope(),
- ) {
- const options = this.getOptions();
- const integrations = Object.keys(this._integrations);
- if (!hint.integrations && integrations.length > 0) {
- hint.integrations = integrations;
- }
- this.emit('preprocessEvent', event, hint);
- return prepareEvent.prepareEvent(options, event, hint, scope, this, isolationScope).then(evt => {
- if (evt === null) {
- return evt;
- }
- const propagationContext = {
- ...isolationScope.getPropagationContext(),
- ...(scope ? scope.getPropagationContext() : undefined),
- };
- const trace = evt.contexts && evt.contexts.trace;
- if (!trace && propagationContext) {
- const { traceId: trace_id, spanId, parentSpanId, dsc } = propagationContext;
- evt.contexts = {
- trace: {
- trace_id,
- span_id: spanId,
- parent_span_id: parentSpanId,
- },
- ...evt.contexts,
- };
- const dynamicSamplingContext$1 = dsc ? dsc : dynamicSamplingContext.getDynamicSamplingContextFromClient(trace_id, this, scope);
- evt.sdkProcessingMetadata = {
- dynamicSamplingContext: dynamicSamplingContext$1,
- ...evt.sdkProcessingMetadata,
- };
- }
- return evt;
- });
- }
- /**
- * 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 => {
- if (debugBuild.DEBUG_BUILD) {
- // If something's gone wrong, log the error as a warning. If it's just us having used a `SentryError` for
- // control flow, log just the message (no stack) as a log-level log.
- const sentryError = reason ;
- if (sentryError.logLevel === 'log') {
- utils.logger.log(sentryError.message);
- } else {
- utils.logger.warn(sentryError);
- }
- }
- 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 options = this.getOptions();
- const { sampleRate } = options;
- const isTransaction = isTransactionEvent(event);
- const isError = isErrorEvent(event);
- const eventType = event.type || 'error';
- const beforeSendLabel = `before send for type \`${eventType}\``;
- // 1.0 === 100% events are sent
- // 0.0 === 0% events are sent
- // Sampling for transaction happens somewhere else
- if (isError && typeof sampleRate === 'number' && Math.random() > sampleRate) {
- this.recordDroppedEvent('sample_rate', 'error', event);
- return utils.rejectedSyncPromise(
- new utils.SentryError(
- `Discarding event because it's not included in the random sample (sampling rate = ${sampleRate})`,
- 'log',
- ),
- );
- }
- const dataCategory = eventType === 'replay_event' ? 'replay' : eventType;
- const sdkProcessingMetadata = event.sdkProcessingMetadata || {};
- const capturedSpanIsolationScope = sdkProcessingMetadata.capturedSpanIsolationScope;
- return this._prepareEvent(event, hint, scope, capturedSpanIsolationScope)
- .then(prepared => {
- if (prepared === null) {
- this.recordDroppedEvent('event_processor', dataCategory, event);
- throw new utils.SentryError('An event processor returned `null`, will not send event.', 'log');
- }
- const isInternalException = hint.data && (hint.data ).__sentry__ === true;
- if (isInternalException) {
- return prepared;
- }
- const result = processBeforeSend(options, prepared, hint);
- return _validateBeforeSendResult(result, beforeSendLabel);
- })
- .then(processedEvent => {
- if (processedEvent === null) {
- this.recordDroppedEvent('before_send', dataCategory, event);
- throw new utils.SentryError(`${beforeSendLabel} returned \`null\`, will not send event.`, 'log');
- }
- const session = scope && scope.getSession();
- if (!isTransaction && session) {
- this._updateSessionFromEvent(session, processedEvent);
- }
- // None of the Sentry built event processor will update transaction name,
- // so if the transaction name has been changed by an event processor, we know
- // it has to come from custom event processor added by a user
- const transactionInfo = processedEvent.transaction_info;
- if (isTransaction && transactionInfo && processedEvent.transaction !== event.transaction) {
- const source = 'custom';
- processedEvent.transaction_info = {
- ...transactionInfo,
- source,
- };
- }
- 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++;
- void promise.then(
- value => {
- this._numProcessing--;
- return value;
- },
- reason => {
- this._numProcessing--;
- return reason;
- },
- );
- }
- /**
- * @inheritdoc
- */
- _sendEnvelope(envelope) {
- this.emit('beforeEnvelope', envelope);
- if (this._isEnabled() && this._transport) {
- return this._transport.send(envelope).then(null, reason => {
- debugBuild.DEBUG_BUILD && utils.logger.error('Error while sending event:', reason);
- });
- } else {
- debugBuild.DEBUG_BUILD && utils.logger.error('Transport disabled');
- }
- }
- /**
- * Clears outcomes on this client and returns them.
- */
- _clearOutcomes() {
- const outcomes = this._outcomes;
- this._outcomes = {};
- return Object.keys(outcomes).map(key => {
- const [reason, category] = key.split(':') ;
- return {
- reason,
- category,
- quantity: outcomes[key],
- };
- });
- }
- /**
- * @inheritDoc
- */
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
- }
- /**
- * Verifies that return value of configured `beforeSend` or `beforeSendTransaction` is of expected type, and returns the value if so.
- */
- function _validateBeforeSendResult(
- beforeSendResult,
- beforeSendLabel,
- ) {
- const invalidValueError = `${beforeSendLabel} must return \`null\` or a valid event.`;
- if (utils.isThenable(beforeSendResult)) {
- return beforeSendResult.then(
- event => {
- if (!utils.isPlainObject(event) && event !== null) {
- throw new utils.SentryError(invalidValueError);
- }
- return event;
- },
- e => {
- throw new utils.SentryError(`${beforeSendLabel} rejected with ${e}`);
- },
- );
- } else if (!utils.isPlainObject(beforeSendResult) && beforeSendResult !== null) {
- throw new utils.SentryError(invalidValueError);
- }
- return beforeSendResult;
- }
- /**
- * Process the matching `beforeSendXXX` callback.
- */
- function processBeforeSend(
- options,
- event,
- hint,
- ) {
- const { beforeSend, beforeSendTransaction } = options;
- if (isErrorEvent(event) && beforeSend) {
- return beforeSend(event, hint);
- }
- if (isTransactionEvent(event) && beforeSendTransaction) {
- return beforeSendTransaction(event, hint);
- }
- return event;
- }
- function isErrorEvent(event) {
- return event.type === undefined;
- }
- function isTransactionEvent(event) {
- return event.type === 'transaction';
- }
- /**
- * Add an event processor to the current client.
- * This event processor will run for all events processed by this client.
- */
- function addEventProcessor(callback) {
- const client = exports$1.getClient();
- if (!client || !client.addEventProcessor) {
- return;
- }
- client.addEventProcessor(callback);
- }
- exports.BaseClient = BaseClient;
- exports.addEventProcessor = addEventProcessor;
- //# sourceMappingURL=baseclient.js.map
|