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 | var ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured.";
|
8 |
|
9 |
|
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 | class BaseClient {
|
41 |
|
42 |
|
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 | captureException(exception, hint, scope) {
|
83 |
|
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 |
|
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 |
|
130 |
|
131 | captureEvent(event, hint, scope) {
|
132 |
|
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 |
|
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 |
|
163 | updateSession(session, { init: false });
|
164 | }
|
165 | }
|
166 |
|
167 | |
168 |
|
169 |
|
170 | getDsn() {
|
171 | return this._dsn;
|
172 | }
|
173 |
|
174 | |
175 |
|
176 |
|
177 | getOptions() {
|
178 | return this._options;
|
179 | }
|
180 |
|
181 | |
182 |
|
183 |
|
184 | getTransport() {
|
185 | return this._transport;
|
186 | }
|
187 |
|
188 | |
189 |
|
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 |
|
204 |
|
205 | close(timeout) {
|
206 | return this.flush(timeout).then(result => {
|
207 | this.getOptions().enabled = false;
|
208 | return result;
|
209 | });
|
210 | }
|
211 |
|
212 | |
213 |
|
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 |
|
224 |
|
225 |
|
226 |
|
227 | getIntegrationById(integrationId) {
|
228 | return this._integrations[integrationId];
|
229 | }
|
230 |
|
231 | |
232 |
|
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 |
|
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 |
|
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 |
|
276 |
|
277 | recordDroppedEvent(reason, category) {
|
278 | if (this._options.sendClientReports) {
|
279 |
|
280 |
|
281 |
|
282 |
|
283 |
|
284 |
|
285 | var key = `${reason}:${category}`;
|
286 | (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log(`Adding outcome: "${key}"`);
|
287 |
|
288 |
|
289 | this._outcomes[key] = this._outcomes[key] + 1 || 1;
|
290 | }
|
291 | }
|
292 |
|
293 |
|
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 |
|
312 |
|
313 |
|
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 |
|
328 |
|
329 |
|
330 |
|
331 |
|
332 |
|
333 |
|
334 |
|
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 |
|
357 | _isEnabled() {
|
358 | return this.getOptions().enabled !== false && this._dsn !== undefined;
|
359 | }
|
360 |
|
361 | |
362 |
|
363 |
|
364 |
|
365 |
|
366 |
|
367 |
|
368 |
|
369 |
|
370 |
|
371 |
|
372 |
|
373 |
|
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 |
|
387 |
|
388 | let finalScope = scope;
|
389 | if (hint.captureContext) {
|
390 | finalScope = Scope.clone(finalScope).update(hint.captureContext);
|
391 | }
|
392 |
|
393 |
|
394 | let result = resolvedSyncPromise(prepared);
|
395 |
|
396 |
|
397 |
|
398 | if (finalScope) {
|
399 |
|
400 | var attachments = [...(hint.attachments || []), ...finalScope.getAttachments()];
|
401 |
|
402 | if (attachments.length) {
|
403 | hint.attachments = attachments;
|
404 | }
|
405 |
|
406 |
|
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 |
|
420 |
|
421 |
|
422 |
|
423 |
|
424 |
|
425 |
|
426 |
|
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 |
|
455 |
|
456 |
|
457 |
|
458 |
|
459 |
|
460 |
|
461 | if (event.contexts && event.contexts.trace && normalized.contexts) {
|
462 | normalized.contexts.trace = event.contexts.trace;
|
463 |
|
464 |
|
465 | if (event.contexts.trace.data) {
|
466 | normalized.contexts.trace.data = normalize(event.contexts.trace.data, depth, maxBreadth);
|
467 | }
|
468 | }
|
469 |
|
470 |
|
471 | if (event.spans) {
|
472 | normalized.spans = event.spans.map(span => {
|
473 |
|
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 |
|
486 |
|
487 |
|
488 |
|
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 |
|
523 |
|
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 |
|
535 |
|
536 |
|
537 |
|
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 |
|
553 |
|
554 |
|
555 |
|
556 |
|
557 |
|
558 |
|
559 |
|
560 |
|
561 |
|
562 |
|
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 |
|
573 |
|
574 |
|
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 |
|
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 |
|
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 |
|
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 |
|
678 |
|
679 |
|
680 |
|
681 | }
|
682 |
|
683 |
|
684 |
|
685 |
|
686 | function _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 |
|
706 | export { BaseClient };
|
707 |
|