1 | import { updateSession, Scope } from '@sentry/hub';
|
2 | import { makeDsn, logger, checkOrSetAlreadyCaught, isPrimitive, resolvedSyncPromise, addItemToEnvelope, createAttachmentEnvelopeItem, SyncPromise, uuid4, dateTimestampInSeconds, normalize, truncate, rejectedSyncPromise, SentryError, isThenable, isPlainObject } from '@sentry/utils';
|
3 | import { getEnvelopeEndpointWithUrlEncodedAuth } from './api.js';
|
4 | import { createEventEnvelope, createSessionEnvelope } from './envelope.js';
|
5 | import { setupIntegrations } from './integration.js';
|
6 |
|
7 |
|
8 |
|
9 | var ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured.";
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 | class BaseClient {
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 | __init() {this._integrations = {};}
|
49 |
|
50 |
|
51 | __init2() {this._integrationsInitialized = false;}
|
52 |
|
53 |
|
54 | __init3() {this._numProcessing = 0;}
|
55 |
|
56 |
|
57 | __init4() {this._outcomes = {};}
|
58 |
|
59 | |
60 |
|
61 |
|
62 |
|
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 |
|
81 |
|
82 |
|
83 | captureException(exception, hint, scope) {
|
84 |
|
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 |
|
105 |
|
106 | captureMessage(
|
107 | message,
|
108 |
|
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 |
|
132 |
|
133 | captureEvent(event, hint, scope) {
|
134 |
|
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 |
|
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 |
|
165 | updateSession(session, { init: false });
|
166 | }
|
167 | }
|
168 |
|
169 | |
170 |
|
171 |
|
172 | getDsn() {
|
173 | return this._dsn;
|
174 | }
|
175 |
|
176 | |
177 |
|
178 |
|
179 | getOptions() {
|
180 | return this._options;
|
181 | }
|
182 |
|
183 | |
184 |
|
185 |
|
186 | getTransport() {
|
187 | return this._transport;
|
188 | }
|
189 |
|
190 | |
191 |
|
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 |
|
206 |
|
207 | close(timeout) {
|
208 | return this.flush(timeout).then(result => {
|
209 | this.getOptions().enabled = false;
|
210 | return result;
|
211 | });
|
212 | }
|
213 |
|
214 | |
215 |
|
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 |
|
226 |
|
227 |
|
228 |
|
229 | getIntegrationById(integrationId) {
|
230 | return this._integrations[integrationId];
|
231 | }
|
232 |
|
233 | |
234 |
|
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 |
|
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 |
|
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 |
|
278 |
|
279 | recordDroppedEvent(reason, category) {
|
280 | if (this._options.sendClientReports) {
|
281 |
|
282 |
|
283 |
|
284 |
|
285 |
|
286 |
|
287 | var key = `${reason}:${category}`;
|
288 | (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log(`Adding outcome: "${key}"`);
|
289 |
|
290 |
|
291 | this._outcomes[key] = this._outcomes[key] + 1 || 1;
|
292 | }
|
293 | }
|
294 |
|
295 |
|
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 |
|
314 |
|
315 |
|
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 |
|
330 |
|
331 |
|
332 |
|
333 |
|
334 |
|
335 |
|
336 |
|
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 |
|
359 | _isEnabled() {
|
360 | return this.getOptions().enabled !== false && this._dsn !== undefined;
|
361 | }
|
362 |
|
363 | |
364 |
|
365 |
|
366 |
|
367 |
|
368 |
|
369 |
|
370 |
|
371 |
|
372 |
|
373 |
|
374 |
|
375 |
|
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 |
|
389 |
|
390 | let finalScope = scope;
|
391 | if (hint.captureContext) {
|
392 | finalScope = Scope.clone(finalScope).update(hint.captureContext);
|
393 | }
|
394 |
|
395 |
|
396 | let result = resolvedSyncPromise(prepared);
|
397 |
|
398 |
|
399 |
|
400 | if (finalScope) {
|
401 |
|
402 | var attachments = [...(hint.attachments || []), ...finalScope.getAttachments()];
|
403 |
|
404 | if (attachments.length) {
|
405 | hint.attachments = attachments;
|
406 | }
|
407 |
|
408 |
|
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 |
|
422 |
|
423 |
|
424 |
|
425 |
|
426 |
|
427 |
|
428 |
|
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 |
|
457 |
|
458 |
|
459 |
|
460 |
|
461 |
|
462 |
|
463 | if (event.contexts && event.contexts.trace && normalized.contexts) {
|
464 | normalized.contexts.trace = event.contexts.trace;
|
465 |
|
466 |
|
467 | if (event.contexts.trace.data) {
|
468 | normalized.contexts.trace.data = normalize(event.contexts.trace.data, depth, maxBreadth);
|
469 | }
|
470 | }
|
471 |
|
472 |
|
473 | if (event.spans) {
|
474 | normalized.spans = event.spans.map(span => {
|
475 |
|
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 |
|
488 |
|
489 |
|
490 |
|
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 |
|
525 |
|
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 |
|
537 |
|
538 |
|
539 |
|
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 |
|
549 |
|
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 |
|
564 |
|
565 |
|
566 |
|
567 |
|
568 |
|
569 |
|
570 |
|
571 |
|
572 |
|
573 |
|
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 |
|
584 |
|
585 |
|
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 |
|
623 |
|
624 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
711 |
|
712 |
|
713 |
|
714 | }
|
715 |
|
716 |
|
717 |
|
718 |
|
719 | function _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 |
|
739 | export { BaseClient };
|
740 |
|