baseclient.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808
  1. import { makeDsn, logger, checkOrSetAlreadyCaught, isParameterizedString, isPrimitive, resolvedSyncPromise, addItemToEnvelope, createAttachmentEnvelopeItem, SyncPromise, rejectedSyncPromise, SentryError, isThenable, isPlainObject } from '@sentry/utils';
  2. import { getEnvelopeEndpointWithUrlEncodedAuth } from './api.js';
  3. import { DEBUG_BUILD } from './debug-build.js';
  4. import { createEventEnvelope, createSessionEnvelope } from './envelope.js';
  5. import { getClient } from './exports.js';
  6. import { getIsolationScope } from './hub.js';
  7. import { setupIntegration, afterSetupIntegrations, setupIntegrations } from './integration.js';
  8. import { createMetricEnvelope } from './metrics/envelope.js';
  9. import { updateSession } from './session.js';
  10. import { getDynamicSamplingContextFromClient } from './tracing/dynamicSamplingContext.js';
  11. import { prepareEvent } from './utils/prepareEvent.js';
  12. const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured.";
  13. /**
  14. * Base implementation for all JavaScript SDK clients.
  15. *
  16. * Call the constructor with the corresponding options
  17. * specific to the client subclass. To access these options later, use
  18. * {@link Client.getOptions}.
  19. *
  20. * If a Dsn is specified in the options, it will be parsed and stored. Use
  21. * {@link Client.getDsn} to retrieve the Dsn at any moment. In case the Dsn is
  22. * invalid, the constructor will throw a {@link SentryException}. Note that
  23. * without a valid Dsn, the SDK will not send any events to Sentry.
  24. *
  25. * Before sending an event, it is passed through
  26. * {@link BaseClient._prepareEvent} to add SDK information and scope data
  27. * (breadcrumbs and context). To add more custom information, override this
  28. * method and extend the resulting prepared event.
  29. *
  30. * To issue automatically created events (e.g. via instrumentation), use
  31. * {@link Client.captureEvent}. It will prepare the event and pass it through
  32. * the callback lifecycle. To issue auto-breadcrumbs, use
  33. * {@link Client.addBreadcrumb}.
  34. *
  35. * @example
  36. * class NodeClient extends BaseClient<NodeOptions> {
  37. * public constructor(options: NodeOptions) {
  38. * super(options);
  39. * }
  40. *
  41. * // ...
  42. * }
  43. */
  44. class BaseClient {
  45. /**
  46. * A reference to a metrics aggregator
  47. *
  48. * @experimental Note this is alpha API. It may experience breaking changes in the future.
  49. */
  50. /** Options passed to the SDK. */
  51. /** The client Dsn, if specified in options. Without this Dsn, the SDK will be disabled. */
  52. /** Array of set up integrations. */
  53. /** Indicates whether this client's integrations have been set up. */
  54. /** Number of calls being processed */
  55. /** Holds flushable */
  56. // eslint-disable-next-line @typescript-eslint/ban-types
  57. /**
  58. * Initializes this client instance.
  59. *
  60. * @param options Options for the client.
  61. */
  62. constructor(options) {
  63. this._options = options;
  64. this._integrations = {};
  65. this._integrationsInitialized = false;
  66. this._numProcessing = 0;
  67. this._outcomes = {};
  68. this._hooks = {};
  69. this._eventProcessors = [];
  70. if (options.dsn) {
  71. this._dsn = makeDsn(options.dsn);
  72. } else {
  73. DEBUG_BUILD && logger.warn('No DSN provided, client will not send events.');
  74. }
  75. if (this._dsn) {
  76. const url = getEnvelopeEndpointWithUrlEncodedAuth(this._dsn, options);
  77. this._transport = options.transport({
  78. recordDroppedEvent: this.recordDroppedEvent.bind(this),
  79. ...options.transportOptions,
  80. url,
  81. });
  82. }
  83. }
  84. /**
  85. * @inheritDoc
  86. */
  87. // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
  88. captureException(exception, hint, scope) {
  89. // ensure we haven't captured this very object before
  90. if (checkOrSetAlreadyCaught(exception)) {
  91. DEBUG_BUILD && logger.log(ALREADY_SEEN_ERROR);
  92. return;
  93. }
  94. let eventId = hint && hint.event_id;
  95. this._process(
  96. this.eventFromException(exception, hint)
  97. .then(event => this._captureEvent(event, hint, scope))
  98. .then(result => {
  99. eventId = result;
  100. }),
  101. );
  102. return eventId;
  103. }
  104. /**
  105. * @inheritDoc
  106. */
  107. captureMessage(
  108. message,
  109. // eslint-disable-next-line deprecation/deprecation
  110. level,
  111. hint,
  112. scope,
  113. ) {
  114. let eventId = hint && hint.event_id;
  115. const eventMessage = isParameterizedString(message) ? message : String(message);
  116. const promisedEvent = isPrimitive(message)
  117. ? this.eventFromMessage(eventMessage, level, hint)
  118. : this.eventFromException(message, hint);
  119. this._process(
  120. promisedEvent
  121. .then(event => this._captureEvent(event, hint, scope))
  122. .then(result => {
  123. eventId = result;
  124. }),
  125. );
  126. return eventId;
  127. }
  128. /**
  129. * @inheritDoc
  130. */
  131. captureEvent(event, hint, scope) {
  132. // ensure we haven't captured this very object before
  133. if (hint && hint.originalException && checkOrSetAlreadyCaught(hint.originalException)) {
  134. DEBUG_BUILD && logger.log(ALREADY_SEEN_ERROR);
  135. return;
  136. }
  137. let eventId = hint && hint.event_id;
  138. const sdkProcessingMetadata = event.sdkProcessingMetadata || {};
  139. const capturedSpanScope = sdkProcessingMetadata.capturedSpanScope;
  140. this._process(
  141. this._captureEvent(event, hint, capturedSpanScope || scope).then(result => {
  142. eventId = result;
  143. }),
  144. );
  145. return eventId;
  146. }
  147. /**
  148. * @inheritDoc
  149. */
  150. captureSession(session) {
  151. if (!(typeof session.release === 'string')) {
  152. DEBUG_BUILD && logger.warn('Discarded session because of missing or non-string release');
  153. } else {
  154. this.sendSession(session);
  155. // After sending, we set init false to indicate it's not the first occurrence
  156. updateSession(session, { init: false });
  157. }
  158. }
  159. /**
  160. * @inheritDoc
  161. */
  162. getDsn() {
  163. return this._dsn;
  164. }
  165. /**
  166. * @inheritDoc
  167. */
  168. getOptions() {
  169. return this._options;
  170. }
  171. /**
  172. * @see SdkMetadata in @sentry/types
  173. *
  174. * @return The metadata of the SDK
  175. */
  176. getSdkMetadata() {
  177. return this._options._metadata;
  178. }
  179. /**
  180. * @inheritDoc
  181. */
  182. getTransport() {
  183. return this._transport;
  184. }
  185. /**
  186. * @inheritDoc
  187. */
  188. flush(timeout) {
  189. const transport = this._transport;
  190. if (transport) {
  191. if (this.metricsAggregator) {
  192. this.metricsAggregator.flush();
  193. }
  194. return this._isClientDoneProcessing(timeout).then(clientFinished => {
  195. return transport.flush(timeout).then(transportFlushed => clientFinished && transportFlushed);
  196. });
  197. } else {
  198. return resolvedSyncPromise(true);
  199. }
  200. }
  201. /**
  202. * @inheritDoc
  203. */
  204. close(timeout) {
  205. return this.flush(timeout).then(result => {
  206. this.getOptions().enabled = false;
  207. if (this.metricsAggregator) {
  208. this.metricsAggregator.close();
  209. }
  210. return result;
  211. });
  212. }
  213. /** Get all installed event processors. */
  214. getEventProcessors() {
  215. return this._eventProcessors;
  216. }
  217. /** @inheritDoc */
  218. addEventProcessor(eventProcessor) {
  219. this._eventProcessors.push(eventProcessor);
  220. }
  221. /**
  222. * This is an internal function to setup all integrations that should run on the client.
  223. * @deprecated Use `client.init()` instead.
  224. */
  225. setupIntegrations(forceInitialize) {
  226. if ((forceInitialize && !this._integrationsInitialized) || (this._isEnabled() && !this._integrationsInitialized)) {
  227. this._setupIntegrations();
  228. }
  229. }
  230. /** @inheritdoc */
  231. init() {
  232. if (this._isEnabled()) {
  233. this._setupIntegrations();
  234. }
  235. }
  236. /**
  237. * Gets an installed integration by its `id`.
  238. *
  239. * @returns The installed integration or `undefined` if no integration with that `id` was installed.
  240. * @deprecated Use `getIntegrationByName()` instead.
  241. */
  242. getIntegrationById(integrationId) {
  243. return this.getIntegrationByName(integrationId);
  244. }
  245. /**
  246. * Gets an installed integration by its name.
  247. *
  248. * @returns The installed integration or `undefined` if no integration with that `name` was installed.
  249. */
  250. getIntegrationByName(integrationName) {
  251. return this._integrations[integrationName] ;
  252. }
  253. /**
  254. * Returns the client's instance of the given integration class, it any.
  255. * @deprecated Use `getIntegrationByName()` instead.
  256. */
  257. getIntegration(integration) {
  258. try {
  259. return (this._integrations[integration.id] ) || null;
  260. } catch (_oO) {
  261. DEBUG_BUILD && logger.warn(`Cannot retrieve integration ${integration.id} from the current Client`);
  262. return null;
  263. }
  264. }
  265. /**
  266. * @inheritDoc
  267. */
  268. addIntegration(integration) {
  269. const isAlreadyInstalled = this._integrations[integration.name];
  270. // This hook takes care of only installing if not already installed
  271. setupIntegration(this, integration, this._integrations);
  272. // Here we need to check manually to make sure to not run this multiple times
  273. if (!isAlreadyInstalled) {
  274. afterSetupIntegrations(this, [integration]);
  275. }
  276. }
  277. /**
  278. * @inheritDoc
  279. */
  280. sendEvent(event, hint = {}) {
  281. this.emit('beforeSendEvent', event, hint);
  282. let env = createEventEnvelope(event, this._dsn, this._options._metadata, this._options.tunnel);
  283. for (const attachment of hint.attachments || []) {
  284. env = addItemToEnvelope(
  285. env,
  286. createAttachmentEnvelopeItem(
  287. attachment,
  288. this._options.transportOptions && this._options.transportOptions.textEncoder,
  289. ),
  290. );
  291. }
  292. const promise = this._sendEnvelope(env);
  293. if (promise) {
  294. promise.then(sendResponse => this.emit('afterSendEvent', event, sendResponse), null);
  295. }
  296. }
  297. /**
  298. * @inheritDoc
  299. */
  300. sendSession(session) {
  301. const env = createSessionEnvelope(session, this._dsn, this._options._metadata, this._options.tunnel);
  302. // _sendEnvelope should not throw
  303. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  304. this._sendEnvelope(env);
  305. }
  306. /**
  307. * @inheritDoc
  308. */
  309. recordDroppedEvent(reason, category, _event) {
  310. // Note: we use `event` in replay, where we overwrite this hook.
  311. if (this._options.sendClientReports) {
  312. // We want to track each category (error, transaction, session, replay_event) separately
  313. // but still keep the distinction between different type of outcomes.
  314. // We could use nested maps, but it's much easier to read and type this way.
  315. // A correct type for map-based implementation if we want to go that route
  316. // would be `Partial<Record<SentryRequestType, Partial<Record<Outcome, number>>>>`
  317. // With typescript 4.1 we could even use template literal types
  318. const key = `${reason}:${category}`;
  319. DEBUG_BUILD && logger.log(`Adding outcome: "${key}"`);
  320. // The following works because undefined + 1 === NaN and NaN is falsy
  321. this._outcomes[key] = this._outcomes[key] + 1 || 1;
  322. }
  323. }
  324. /**
  325. * @inheritDoc
  326. */
  327. captureAggregateMetrics(metricBucketItems) {
  328. DEBUG_BUILD && logger.log(`Flushing aggregated metrics, number of metrics: ${metricBucketItems.length}`);
  329. const metricsEnvelope = createMetricEnvelope(
  330. metricBucketItems,
  331. this._dsn,
  332. this._options._metadata,
  333. this._options.tunnel,
  334. );
  335. // _sendEnvelope should not throw
  336. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  337. this._sendEnvelope(metricsEnvelope);
  338. }
  339. // Keep on() & emit() signatures in sync with types' client.ts interface
  340. /* eslint-disable @typescript-eslint/unified-signatures */
  341. /** @inheritdoc */
  342. /** @inheritdoc */
  343. on(hook, callback) {
  344. if (!this._hooks[hook]) {
  345. this._hooks[hook] = [];
  346. }
  347. // @ts-expect-error We assue the types are correct
  348. this._hooks[hook].push(callback);
  349. }
  350. /** @inheritdoc */
  351. /** @inheritdoc */
  352. emit(hook, ...rest) {
  353. if (this._hooks[hook]) {
  354. this._hooks[hook].forEach(callback => callback(...rest));
  355. }
  356. }
  357. /* eslint-enable @typescript-eslint/unified-signatures */
  358. /** Setup integrations for this client. */
  359. _setupIntegrations() {
  360. const { integrations } = this._options;
  361. this._integrations = setupIntegrations(this, integrations);
  362. afterSetupIntegrations(this, integrations);
  363. // TODO v8: We don't need this flag anymore
  364. this._integrationsInitialized = true;
  365. }
  366. /** Updates existing session based on the provided event */
  367. _updateSessionFromEvent(session, event) {
  368. let crashed = false;
  369. let errored = false;
  370. const exceptions = event.exception && event.exception.values;
  371. if (exceptions) {
  372. errored = true;
  373. for (const ex of exceptions) {
  374. const mechanism = ex.mechanism;
  375. if (mechanism && mechanism.handled === false) {
  376. crashed = true;
  377. break;
  378. }
  379. }
  380. }
  381. // A session is updated and that session update is sent in only one of the two following scenarios:
  382. // 1. Session with non terminal status and 0 errors + an error occurred -> Will set error count to 1 and send update
  383. // 2. Session with non terminal status and 1 error + a crash occurred -> Will set status crashed and send update
  384. const sessionNonTerminal = session.status === 'ok';
  385. const shouldUpdateAndSend = (sessionNonTerminal && session.errors === 0) || (sessionNonTerminal && crashed);
  386. if (shouldUpdateAndSend) {
  387. updateSession(session, {
  388. ...(crashed && { status: 'crashed' }),
  389. errors: session.errors || Number(errored || crashed),
  390. });
  391. this.captureSession(session);
  392. }
  393. }
  394. /**
  395. * Determine if the client is finished processing. Returns a promise because it will wait `timeout` ms before saying
  396. * "no" (resolving to `false`) in order to give the client a chance to potentially finish first.
  397. *
  398. * @param timeout The time, in ms, after which to resolve to `false` if the client is still busy. Passing `0` (or not
  399. * passing anything) will make the promise wait as long as it takes for processing to finish before resolving to
  400. * `true`.
  401. * @returns A promise which will resolve to `true` if processing is already done or finishes before the timeout, and
  402. * `false` otherwise
  403. */
  404. _isClientDoneProcessing(timeout) {
  405. return new SyncPromise(resolve => {
  406. let ticked = 0;
  407. const tick = 1;
  408. const interval = setInterval(() => {
  409. if (this._numProcessing == 0) {
  410. clearInterval(interval);
  411. resolve(true);
  412. } else {
  413. ticked += tick;
  414. if (timeout && ticked >= timeout) {
  415. clearInterval(interval);
  416. resolve(false);
  417. }
  418. }
  419. }, tick);
  420. });
  421. }
  422. /** Determines whether this SDK is enabled and a transport is present. */
  423. _isEnabled() {
  424. return this.getOptions().enabled !== false && this._transport !== undefined;
  425. }
  426. /**
  427. * Adds common information to events.
  428. *
  429. * The information includes release and environment from `options`,
  430. * breadcrumbs and context (extra, tags and user) from the scope.
  431. *
  432. * Information that is already present in the event is never overwritten. For
  433. * nested objects, such as the context, keys are merged.
  434. *
  435. * @param event The original event.
  436. * @param hint May contain additional information about the original exception.
  437. * @param scope A scope containing event metadata.
  438. * @returns A new event with more information.
  439. */
  440. _prepareEvent(
  441. event,
  442. hint,
  443. scope,
  444. isolationScope = getIsolationScope(),
  445. ) {
  446. const options = this.getOptions();
  447. const integrations = Object.keys(this._integrations);
  448. if (!hint.integrations && integrations.length > 0) {
  449. hint.integrations = integrations;
  450. }
  451. this.emit('preprocessEvent', event, hint);
  452. return prepareEvent(options, event, hint, scope, this, isolationScope).then(evt => {
  453. if (evt === null) {
  454. return evt;
  455. }
  456. const propagationContext = {
  457. ...isolationScope.getPropagationContext(),
  458. ...(scope ? scope.getPropagationContext() : undefined),
  459. };
  460. const trace = evt.contexts && evt.contexts.trace;
  461. if (!trace && propagationContext) {
  462. const { traceId: trace_id, spanId, parentSpanId, dsc } = propagationContext;
  463. evt.contexts = {
  464. trace: {
  465. trace_id,
  466. span_id: spanId,
  467. parent_span_id: parentSpanId,
  468. },
  469. ...evt.contexts,
  470. };
  471. const dynamicSamplingContext = dsc ? dsc : getDynamicSamplingContextFromClient(trace_id, this, scope);
  472. evt.sdkProcessingMetadata = {
  473. dynamicSamplingContext,
  474. ...evt.sdkProcessingMetadata,
  475. };
  476. }
  477. return evt;
  478. });
  479. }
  480. /**
  481. * Processes the event and logs an error in case of rejection
  482. * @param event
  483. * @param hint
  484. * @param scope
  485. */
  486. _captureEvent(event, hint = {}, scope) {
  487. return this._processEvent(event, hint, scope).then(
  488. finalEvent => {
  489. return finalEvent.event_id;
  490. },
  491. reason => {
  492. if (DEBUG_BUILD) {
  493. // If something's gone wrong, log the error as a warning. If it's just us having used a `SentryError` for
  494. // control flow, log just the message (no stack) as a log-level log.
  495. const sentryError = reason ;
  496. if (sentryError.logLevel === 'log') {
  497. logger.log(sentryError.message);
  498. } else {
  499. logger.warn(sentryError);
  500. }
  501. }
  502. return undefined;
  503. },
  504. );
  505. }
  506. /**
  507. * Processes an event (either error or message) and sends it to Sentry.
  508. *
  509. * This also adds breadcrumbs and context information to the event. However,
  510. * platform specific meta data (such as the User's IP address) must be added
  511. * by the SDK implementor.
  512. *
  513. *
  514. * @param event The event to send to Sentry.
  515. * @param hint May contain additional information about the original exception.
  516. * @param scope A scope containing event metadata.
  517. * @returns A SyncPromise that resolves with the event or rejects in case event was/will not be send.
  518. */
  519. _processEvent(event, hint, scope) {
  520. const options = this.getOptions();
  521. const { sampleRate } = options;
  522. const isTransaction = isTransactionEvent(event);
  523. const isError = isErrorEvent(event);
  524. const eventType = event.type || 'error';
  525. const beforeSendLabel = `before send for type \`${eventType}\``;
  526. // 1.0 === 100% events are sent
  527. // 0.0 === 0% events are sent
  528. // Sampling for transaction happens somewhere else
  529. if (isError && typeof sampleRate === 'number' && Math.random() > sampleRate) {
  530. this.recordDroppedEvent('sample_rate', 'error', event);
  531. return rejectedSyncPromise(
  532. new SentryError(
  533. `Discarding event because it's not included in the random sample (sampling rate = ${sampleRate})`,
  534. 'log',
  535. ),
  536. );
  537. }
  538. const dataCategory = eventType === 'replay_event' ? 'replay' : eventType;
  539. const sdkProcessingMetadata = event.sdkProcessingMetadata || {};
  540. const capturedSpanIsolationScope = sdkProcessingMetadata.capturedSpanIsolationScope;
  541. return this._prepareEvent(event, hint, scope, capturedSpanIsolationScope)
  542. .then(prepared => {
  543. if (prepared === null) {
  544. this.recordDroppedEvent('event_processor', dataCategory, event);
  545. throw new SentryError('An event processor returned `null`, will not send event.', 'log');
  546. }
  547. const isInternalException = hint.data && (hint.data ).__sentry__ === true;
  548. if (isInternalException) {
  549. return prepared;
  550. }
  551. const result = processBeforeSend(options, prepared, hint);
  552. return _validateBeforeSendResult(result, beforeSendLabel);
  553. })
  554. .then(processedEvent => {
  555. if (processedEvent === null) {
  556. this.recordDroppedEvent('before_send', dataCategory, event);
  557. throw new SentryError(`${beforeSendLabel} returned \`null\`, will not send event.`, 'log');
  558. }
  559. const session = scope && scope.getSession();
  560. if (!isTransaction && session) {
  561. this._updateSessionFromEvent(session, processedEvent);
  562. }
  563. // None of the Sentry built event processor will update transaction name,
  564. // so if the transaction name has been changed by an event processor, we know
  565. // it has to come from custom event processor added by a user
  566. const transactionInfo = processedEvent.transaction_info;
  567. if (isTransaction && transactionInfo && processedEvent.transaction !== event.transaction) {
  568. const source = 'custom';
  569. processedEvent.transaction_info = {
  570. ...transactionInfo,
  571. source,
  572. };
  573. }
  574. this.sendEvent(processedEvent, hint);
  575. return processedEvent;
  576. })
  577. .then(null, reason => {
  578. if (reason instanceof SentryError) {
  579. throw reason;
  580. }
  581. this.captureException(reason, {
  582. data: {
  583. __sentry__: true,
  584. },
  585. originalException: reason,
  586. });
  587. throw new SentryError(
  588. `Event processing pipeline threw an error, original event will not be sent. Details have been sent as a new event.\nReason: ${reason}`,
  589. );
  590. });
  591. }
  592. /**
  593. * Occupies the client with processing and event
  594. */
  595. _process(promise) {
  596. this._numProcessing++;
  597. void promise.then(
  598. value => {
  599. this._numProcessing--;
  600. return value;
  601. },
  602. reason => {
  603. this._numProcessing--;
  604. return reason;
  605. },
  606. );
  607. }
  608. /**
  609. * @inheritdoc
  610. */
  611. _sendEnvelope(envelope) {
  612. this.emit('beforeEnvelope', envelope);
  613. if (this._isEnabled() && this._transport) {
  614. return this._transport.send(envelope).then(null, reason => {
  615. DEBUG_BUILD && logger.error('Error while sending event:', reason);
  616. });
  617. } else {
  618. DEBUG_BUILD && logger.error('Transport disabled');
  619. }
  620. }
  621. /**
  622. * Clears outcomes on this client and returns them.
  623. */
  624. _clearOutcomes() {
  625. const outcomes = this._outcomes;
  626. this._outcomes = {};
  627. return Object.keys(outcomes).map(key => {
  628. const [reason, category] = key.split(':') ;
  629. return {
  630. reason,
  631. category,
  632. quantity: outcomes[key],
  633. };
  634. });
  635. }
  636. /**
  637. * @inheritDoc
  638. */
  639. // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
  640. }
  641. /**
  642. * Verifies that return value of configured `beforeSend` or `beforeSendTransaction` is of expected type, and returns the value if so.
  643. */
  644. function _validateBeforeSendResult(
  645. beforeSendResult,
  646. beforeSendLabel,
  647. ) {
  648. const invalidValueError = `${beforeSendLabel} must return \`null\` or a valid event.`;
  649. if (isThenable(beforeSendResult)) {
  650. return beforeSendResult.then(
  651. event => {
  652. if (!isPlainObject(event) && event !== null) {
  653. throw new SentryError(invalidValueError);
  654. }
  655. return event;
  656. },
  657. e => {
  658. throw new SentryError(`${beforeSendLabel} rejected with ${e}`);
  659. },
  660. );
  661. } else if (!isPlainObject(beforeSendResult) && beforeSendResult !== null) {
  662. throw new SentryError(invalidValueError);
  663. }
  664. return beforeSendResult;
  665. }
  666. /**
  667. * Process the matching `beforeSendXXX` callback.
  668. */
  669. function processBeforeSend(
  670. options,
  671. event,
  672. hint,
  673. ) {
  674. const { beforeSend, beforeSendTransaction } = options;
  675. if (isErrorEvent(event) && beforeSend) {
  676. return beforeSend(event, hint);
  677. }
  678. if (isTransactionEvent(event) && beforeSendTransaction) {
  679. return beforeSendTransaction(event, hint);
  680. }
  681. return event;
  682. }
  683. function isErrorEvent(event) {
  684. return event.type === undefined;
  685. }
  686. function isTransactionEvent(event) {
  687. return event.type === 'transaction';
  688. }
  689. /**
  690. * Add an event processor to the current client.
  691. * This event processor will run for all events processed by this client.
  692. */
  693. function addEventProcessor(callback) {
  694. const client = getClient();
  695. if (!client || !client.addEventProcessor) {
  696. return;
  697. }
  698. client.addEventProcessor(callback);
  699. }
  700. export { BaseClient, addEventProcessor };
  701. //# sourceMappingURL=baseclient.js.map