UNPKG

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