@sentry/core
Version:
Base implementation for all Sentry JavaScript SDKs
549 lines (476 loc) • 13.5 kB
JavaScript
import { isPlainObject, dateTimestampInSeconds, SyncPromise, logger, isThenable, arrayify, getGlobalSingleton } from '@sentry/utils';
import { updateSession } from './session.js';
/**
* Default value for maximum number of breadcrumbs added to an event.
*/
const DEFAULT_MAX_BREADCRUMBS = 100;
/**
* Holds additional event information. {@link Scope.applyToEvent} will be
* called by the client before an event will be sent.
*/
class Scope {
/** Flag if notifying is happening. */
/** Callback for client to receive scope changes. */
/** Callback list that will be called after {@link applyToEvent}. */
/** Array of breadcrumbs. */
/** User */
/** Tags */
/** Extra */
/** Contexts */
/** Attachments */
/**
* A place to stash data which is needed at some point in the SDK's event processing pipeline but which shouldn't get
* sent to Sentry
*/
/** Fingerprint */
/** Severity */
// eslint-disable-next-line deprecation/deprecation
/** Transaction Name */
/** Span */
/** Session */
/** Request Mode Session Status */
// NOTE: Any field which gets added here should get added not only to the constructor but also to the `clone` method.
constructor() {
this._notifyingListeners = false;
this._scopeListeners = [];
this._eventProcessors = [];
this._breadcrumbs = [];
this._attachments = [];
this._user = {};
this._tags = {};
this._extra = {};
this._contexts = {};
this._sdkProcessingMetadata = {};
}
/**
* Inherit values from the parent scope.
* @param scope to clone.
*/
static clone(scope) {
const newScope = new Scope();
if (scope) {
newScope._breadcrumbs = [...scope._breadcrumbs];
newScope._tags = { ...scope._tags };
newScope._extra = { ...scope._extra };
newScope._contexts = { ...scope._contexts };
newScope._user = scope._user;
newScope._level = scope._level;
newScope._span = scope._span;
newScope._session = scope._session;
newScope._transactionName = scope._transactionName;
newScope._fingerprint = scope._fingerprint;
newScope._eventProcessors = [...scope._eventProcessors];
newScope._requestSession = scope._requestSession;
newScope._attachments = [...scope._attachments];
newScope._sdkProcessingMetadata = { ...scope._sdkProcessingMetadata };
}
return newScope;
}
/**
* Add internal on change listener. Used for sub SDKs that need to store the scope.
* @hidden
*/
addScopeListener(callback) {
this._scopeListeners.push(callback);
}
/**
* @inheritDoc
*/
addEventProcessor(callback) {
this._eventProcessors.push(callback);
return this;
}
/**
* @inheritDoc
*/
setUser(user) {
this._user = user || {};
if (this._session) {
updateSession(this._session, { user });
}
this._notifyScopeListeners();
return this;
}
/**
* @inheritDoc
*/
getUser() {
return this._user;
}
/**
* @inheritDoc
*/
getRequestSession() {
return this._requestSession;
}
/**
* @inheritDoc
*/
setRequestSession(requestSession) {
this._requestSession = requestSession;
return this;
}
/**
* @inheritDoc
*/
setTags(tags) {
this._tags = {
...this._tags,
...tags,
};
this._notifyScopeListeners();
return this;
}
/**
* @inheritDoc
*/
setTag(key, value) {
this._tags = { ...this._tags, [key]: value };
this._notifyScopeListeners();
return this;
}
/**
* @inheritDoc
*/
setExtras(extras) {
this._extra = {
...this._extra,
...extras,
};
this._notifyScopeListeners();
return this;
}
/**
* @inheritDoc
*/
setExtra(key, extra) {
this._extra = { ...this._extra, [key]: extra };
this._notifyScopeListeners();
return this;
}
/**
* @inheritDoc
*/
setFingerprint(fingerprint) {
this._fingerprint = fingerprint;
this._notifyScopeListeners();
return this;
}
/**
* @inheritDoc
*/
setLevel(
// eslint-disable-next-line deprecation/deprecation
level,
) {
this._level = level;
this._notifyScopeListeners();
return this;
}
/**
* @inheritDoc
*/
setTransactionName(name) {
this._transactionName = name;
this._notifyScopeListeners();
return this;
}
/**
* @inheritDoc
*/
setContext(key, context) {
if (context === null) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this._contexts[key];
} else {
this._contexts[key] = context;
}
this._notifyScopeListeners();
return this;
}
/**
* @inheritDoc
*/
setSpan(span) {
this._span = span;
this._notifyScopeListeners();
return this;
}
/**
* @inheritDoc
*/
getSpan() {
return this._span;
}
/**
* @inheritDoc
*/
getTransaction() {
// Often, this span (if it exists at all) will be a transaction, but it's not guaranteed to be. Regardless, it will
// have a pointer to the currently-active transaction.
const span = this.getSpan();
return span && span.transaction;
}
/**
* @inheritDoc
*/
setSession(session) {
if (!session) {
delete this._session;
} else {
this._session = session;
}
this._notifyScopeListeners();
return this;
}
/**
* @inheritDoc
*/
getSession() {
return this._session;
}
/**
* @inheritDoc
*/
update(captureContext) {
if (!captureContext) {
return this;
}
if (typeof captureContext === 'function') {
const updatedScope = (captureContext )(this);
return updatedScope instanceof Scope ? updatedScope : this;
}
if (captureContext instanceof Scope) {
this._tags = { ...this._tags, ...captureContext._tags };
this._extra = { ...this._extra, ...captureContext._extra };
this._contexts = { ...this._contexts, ...captureContext._contexts };
if (captureContext._user && Object.keys(captureContext._user).length) {
this._user = captureContext._user;
}
if (captureContext._level) {
this._level = captureContext._level;
}
if (captureContext._fingerprint) {
this._fingerprint = captureContext._fingerprint;
}
if (captureContext._requestSession) {
this._requestSession = captureContext._requestSession;
}
} else if (isPlainObject(captureContext)) {
// eslint-disable-next-line no-param-reassign
captureContext = captureContext ;
this._tags = { ...this._tags, ...captureContext.tags };
this._extra = { ...this._extra, ...captureContext.extra };
this._contexts = { ...this._contexts, ...captureContext.contexts };
if (captureContext.user) {
this._user = captureContext.user;
}
if (captureContext.level) {
this._level = captureContext.level;
}
if (captureContext.fingerprint) {
this._fingerprint = captureContext.fingerprint;
}
if (captureContext.requestSession) {
this._requestSession = captureContext.requestSession;
}
}
return this;
}
/**
* @inheritDoc
*/
clear() {
this._breadcrumbs = [];
this._tags = {};
this._extra = {};
this._user = {};
this._contexts = {};
this._level = undefined;
this._transactionName = undefined;
this._fingerprint = undefined;
this._requestSession = undefined;
this._span = undefined;
this._session = undefined;
this._notifyScopeListeners();
this._attachments = [];
return this;
}
/**
* @inheritDoc
*/
addBreadcrumb(breadcrumb, maxBreadcrumbs) {
const maxCrumbs = typeof maxBreadcrumbs === 'number' ? maxBreadcrumbs : DEFAULT_MAX_BREADCRUMBS;
// No data has been changed, so don't notify scope listeners
if (maxCrumbs <= 0) {
return this;
}
const mergedBreadcrumb = {
timestamp: dateTimestampInSeconds(),
...breadcrumb,
};
this._breadcrumbs = [...this._breadcrumbs, mergedBreadcrumb].slice(-maxCrumbs);
this._notifyScopeListeners();
return this;
}
/**
* @inheritDoc
*/
getLastBreadcrumb() {
return this._breadcrumbs[this._breadcrumbs.length - 1];
}
/**
* @inheritDoc
*/
clearBreadcrumbs() {
this._breadcrumbs = [];
this._notifyScopeListeners();
return this;
}
/**
* @inheritDoc
*/
addAttachment(attachment) {
this._attachments.push(attachment);
return this;
}
/**
* @inheritDoc
*/
getAttachments() {
return this._attachments;
}
/**
* @inheritDoc
*/
clearAttachments() {
this._attachments = [];
return this;
}
/**
* Applies data from the scope to the event and runs all event processors on it.
*
* @param event Event
* @param hint Object containing additional information about the original exception, for use by the event processors.
* @hidden
*/
applyToEvent(event, hint = {}) {
if (this._extra && Object.keys(this._extra).length) {
event.extra = { ...this._extra, ...event.extra };
}
if (this._tags && Object.keys(this._tags).length) {
event.tags = { ...this._tags, ...event.tags };
}
if (this._user && Object.keys(this._user).length) {
event.user = { ...this._user, ...event.user };
}
if (this._contexts && Object.keys(this._contexts).length) {
event.contexts = { ...this._contexts, ...event.contexts };
}
if (this._level) {
event.level = this._level;
}
if (this._transactionName) {
event.transaction = this._transactionName;
}
// We want to set the trace context for normal events only if there isn't already
// a trace context on the event. There is a product feature in place where we link
// errors with transaction and it relies on that.
if (this._span) {
event.contexts = { trace: this._span.getTraceContext(), ...event.contexts };
const transactionName = this._span.transaction && this._span.transaction.name;
if (transactionName) {
event.tags = { transaction: transactionName, ...event.tags };
}
}
this._applyFingerprint(event);
event.breadcrumbs = [...(event.breadcrumbs || []), ...this._breadcrumbs];
event.breadcrumbs = event.breadcrumbs.length > 0 ? event.breadcrumbs : undefined;
event.sdkProcessingMetadata = { ...event.sdkProcessingMetadata, ...this._sdkProcessingMetadata };
return this._notifyEventProcessors([...getGlobalEventProcessors(), ...this._eventProcessors], event, hint);
}
/**
* Add data which will be accessible during event processing but won't get sent to Sentry
*/
setSDKProcessingMetadata(newData) {
this._sdkProcessingMetadata = { ...this._sdkProcessingMetadata, ...newData };
return this;
}
/**
* This will be called after {@link applyToEvent} is finished.
*/
_notifyEventProcessors(
processors,
event,
hint,
index = 0,
) {
return new SyncPromise((resolve, reject) => {
const processor = processors[index];
if (event === null || typeof processor !== 'function') {
resolve(event);
} else {
const result = processor({ ...event }, hint) ;
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) &&
processor.id &&
result === null &&
logger.log(`Event processor "${processor.id}" dropped event`);
if (isThenable(result)) {
void result
.then(final => this._notifyEventProcessors(processors, final, hint, index + 1).then(resolve))
.then(null, reject);
} else {
void this._notifyEventProcessors(processors, result, hint, index + 1)
.then(resolve)
.then(null, reject);
}
}
});
}
/**
* This will be called on every set call.
*/
_notifyScopeListeners() {
// We need this check for this._notifyingListeners to be able to work on scope during updates
// If this check is not here we'll produce endless recursion when something is done with the scope
// during the callback.
if (!this._notifyingListeners) {
this._notifyingListeners = true;
this._scopeListeners.forEach(callback => {
callback(this);
});
this._notifyingListeners = false;
}
}
/**
* Applies fingerprint from the scope to the event if there's one,
* uses message if there's one instead or get rid of empty fingerprint
*/
_applyFingerprint(event) {
// Make sure it's an array first and we actually have something in place
event.fingerprint = event.fingerprint ? arrayify(event.fingerprint) : [];
// If we have something on the scope, then merge it with event
if (this._fingerprint) {
event.fingerprint = event.fingerprint.concat(this._fingerprint);
}
// If we have no data at all, remove empty array default
if (event.fingerprint && !event.fingerprint.length) {
delete event.fingerprint;
}
}
}
/**
* Returns the global event processors.
*/
function getGlobalEventProcessors() {
return getGlobalSingleton('globalEventProcessors', () => []);
}
/**
* Add a EventProcessor to be kept globally.
* @param callback EventProcessor to add
*/
function addGlobalEventProcessor(callback) {
getGlobalEventProcessors().push(callback);
}
export { Scope, addGlobalEventProcessor };
//# sourceMappingURL=scope.js.map