UNPKG

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