UNPKG

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