@sentry/core
Version:
Base implementation for all Sentry JavaScript SDKs
586 lines (496 loc) • 15.3 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const session = require('./session.js');
const merge = require('./utils/merge.js');
const spanOnScope = require('./utils/spanOnScope.js');
const is = require('./utils-hoist/is.js');
const logger = require('./utils-hoist/logger.js');
const misc = require('./utils-hoist/misc.js');
const propagationContext = require('./utils-hoist/propagationContext.js');
const string = require('./utils-hoist/string.js');
const time = require('./utils-hoist/time.js');
/**
* Default value for maximum number of breadcrumbs added to an event.
*/
const DEFAULT_MAX_BREADCRUMBS = 100;
/**
* A context to be used for capturing an event.
* This can either be a Scope, or a partial ScopeContext,
* or a callback that receives the current scope and returns a new scope to use.
*/
/**
* Holds additional event information.
*/
class Scope {
/** Flag if notifying is happening. */
/** Callback for client to receive scope changes. */
/** Callback list that will be called during event processing. */
/** Array of breadcrumbs. */
/** User */
/** Tags */
/** Extra */
/** Contexts */
/** Attachments */
/** Propagation Context for distributed tracing */
/**
* 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 */
/**
* Transaction Name
*
* IMPORTANT: The transaction name on the scope has nothing to do with root spans/transaction objects.
* It's purpose is to assign a transaction to the scope that's added to non-transaction events.
*/
/** Session */
/** The client on this scope */
/** Contains the last event id of a captured event. */
// 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 = {};
this._propagationContext = {
traceId: propagationContext.generateTraceId(),
sampleRand: Math.random(),
};
}
/**
* Clone all data from this scope into a new scope.
*/
clone() {
const newScope = new Scope();
newScope._breadcrumbs = [...this._breadcrumbs];
newScope._tags = { ...this._tags };
newScope._extra = { ...this._extra };
newScope._contexts = { ...this._contexts };
if (this._contexts.flags) {
// We need to copy the `values` array so insertions on a cloned scope
// won't affect the original array.
newScope._contexts.flags = {
values: [...this._contexts.flags.values],
};
}
newScope._user = this._user;
newScope._level = this._level;
newScope._session = this._session;
newScope._transactionName = this._transactionName;
newScope._fingerprint = this._fingerprint;
newScope._eventProcessors = [...this._eventProcessors];
newScope._attachments = [...this._attachments];
newScope._sdkProcessingMetadata = { ...this._sdkProcessingMetadata };
newScope._propagationContext = { ...this._propagationContext };
newScope._client = this._client;
newScope._lastEventId = this._lastEventId;
spanOnScope._setSpanForScope(newScope, spanOnScope._getSpanForScope(this));
return newScope;
}
/**
* Update the client assigned to this scope.
* Note that not every scope will have a client assigned - isolation scopes & the global scope will generally not have a client,
* as well as manually created scopes.
*/
setClient(client) {
this._client = client;
}
/**
* Set the ID of the last captured error event.
* This is generally only captured on the isolation scope.
*/
setLastEventId(lastEventId) {
this._lastEventId = lastEventId;
}
/**
* Get the client assigned to this scope.
*/
getClient() {
return this._client ;
}
/**
* Get the ID of the last captured error event.
* This is generally only available on the isolation scope.
*/
lastEventId() {
return this._lastEventId;
}
/**
* @inheritDoc
*/
addScopeListener(callback) {
this._scopeListeners.push(callback);
}
/**
* Add an event processor that will be called before an event is sent.
*/
addEventProcessor(callback) {
this._eventProcessors.push(callback);
return this;
}
/**
* Set the user for this scope.
* Set to `null` to unset the user.
*/
setUser(user) {
// If null is passed we want to unset everything, but still define keys,
// so that later down in the pipeline any existing values are cleared.
this._user = user || {
email: undefined,
id: undefined,
ip_address: undefined,
username: undefined,
};
if (this._session) {
session.updateSession(this._session, { user });
}
this._notifyScopeListeners();
return this;
}
/**
* Get the user from this scope.
*/
getUser() {
return this._user;
}
/**
* Set an object that will be merged into existing tags on the scope,
* and will be sent as tags data with the event.
*/
setTags(tags) {
this._tags = {
...this._tags,
...tags,
};
this._notifyScopeListeners();
return this;
}
/**
* Set a single tag that will be sent as tags data with the event.
*/
setTag(key, value) {
this._tags = { ...this._tags, [key]: value };
this._notifyScopeListeners();
return this;
}
/**
* Set an object that will be merged into existing extra on the scope,
* and will be sent as extra data with the event.
*/
setExtras(extras) {
this._extra = {
...this._extra,
...extras,
};
this._notifyScopeListeners();
return this;
}
/**
* Set a single key:value extra entry that will be sent as extra data with the event.
*/
setExtra(key, extra) {
this._extra = { ...this._extra, [key]: extra };
this._notifyScopeListeners();
return this;
}
/**
* Sets the fingerprint on the scope to send with the events.
* @param {string[]} fingerprint Fingerprint to group events in Sentry.
*/
setFingerprint(fingerprint) {
this._fingerprint = fingerprint;
this._notifyScopeListeners();
return this;
}
/**
* Sets the level on the scope for future events.
*/
setLevel(level) {
this._level = level;
this._notifyScopeListeners();
return this;
}
/**
* Sets the transaction name on the scope so that the name of e.g. taken server route or
* the page location is attached to future events.
*
* IMPORTANT: Calling this function does NOT change the name of the currently active
* root span. If you want to change the name of the active root span, use
* `Sentry.updateSpanName(rootSpan, 'new name')` instead.
*
* By default, the SDK updates the scope's transaction name automatically on sensible
* occasions, such as a page navigation or when handling a new request on the server.
*/
setTransactionName(name) {
this._transactionName = name;
this._notifyScopeListeners();
return this;
}
/**
* Sets context data with the given name.
* Data passed as context will be normalized. You can also pass `null` to unset the context.
* Note that context data will not be merged - calling `setContext` will overwrite an existing context with the same key.
*/
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;
}
/**
* Set the session for the scope.
*/
setSession(session) {
if (!session) {
delete this._session;
} else {
this._session = session;
}
this._notifyScopeListeners();
return this;
}
/**
* Get the session from the scope.
*/
getSession() {
return this._session;
}
/**
* Updates the scope with provided data. Can work in three variations:
* - plain object containing updatable attributes
* - Scope instance that'll extract the attributes from
* - callback function that'll receive the current scope as an argument and allow for modifications
*/
update(captureContext) {
if (!captureContext) {
return this;
}
const scopeToMerge = typeof captureContext === 'function' ? captureContext(this) : captureContext;
const scopeInstance =
scopeToMerge instanceof Scope
? scopeToMerge.getScopeData()
: is.isPlainObject(scopeToMerge)
? (captureContext )
: undefined;
const { tags, extra, user, contexts, level, fingerprint = [], propagationContext } = scopeInstance || {};
this._tags = { ...this._tags, ...tags };
this._extra = { ...this._extra, ...extra };
this._contexts = { ...this._contexts, ...contexts };
if (user && Object.keys(user).length) {
this._user = user;
}
if (level) {
this._level = level;
}
if (fingerprint.length) {
this._fingerprint = fingerprint;
}
if (propagationContext) {
this._propagationContext = propagationContext;
}
return this;
}
/**
* Clears the current scope and resets its properties.
* Note: The client will not be cleared.
*/
clear() {
// client is not cleared here on purpose!
this._breadcrumbs = [];
this._tags = {};
this._extra = {};
this._user = {};
this._contexts = {};
this._level = undefined;
this._transactionName = undefined;
this._fingerprint = undefined;
this._session = undefined;
spanOnScope._setSpanForScope(this, undefined);
this._attachments = [];
this.setPropagationContext({ traceId: propagationContext.generateTraceId(), sampleRand: Math.random() });
this._notifyScopeListeners();
return this;
}
/**
* Adds a breadcrumb to the scope.
* By default, the last 100 breadcrumbs are kept.
*/
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: time.dateTimestampInSeconds(),
...breadcrumb,
// Breadcrumb messages can theoretically be infinitely large and they're held in memory so we truncate them not to leak (too much) memory
message: breadcrumb.message ? string.truncate(breadcrumb.message, 2048) : breadcrumb.message,
};
this._breadcrumbs.push(mergedBreadcrumb);
if (this._breadcrumbs.length > maxCrumbs) {
this._breadcrumbs = this._breadcrumbs.slice(-maxCrumbs);
this._client?.recordDroppedEvent('buffer_overflow', 'log_item');
}
this._notifyScopeListeners();
return this;
}
/**
* Get the last breadcrumb of the scope.
*/
getLastBreadcrumb() {
return this._breadcrumbs[this._breadcrumbs.length - 1];
}
/**
* Clear all breadcrumbs from the scope.
*/
clearBreadcrumbs() {
this._breadcrumbs = [];
this._notifyScopeListeners();
return this;
}
/**
* Add an attachment to the scope.
*/
addAttachment(attachment) {
this._attachments.push(attachment);
return this;
}
/**
* Clear all attachments from the scope.
*/
clearAttachments() {
this._attachments = [];
return this;
}
/**
* Get the data of this scope, which should be applied to an event during processing.
*/
getScopeData() {
return {
breadcrumbs: this._breadcrumbs,
attachments: this._attachments,
contexts: this._contexts,
tags: this._tags,
extra: this._extra,
user: this._user,
level: this._level,
fingerprint: this._fingerprint || [],
eventProcessors: this._eventProcessors,
propagationContext: this._propagationContext,
sdkProcessingMetadata: this._sdkProcessingMetadata,
transactionName: this._transactionName,
span: spanOnScope._getSpanForScope(this),
};
}
/**
* Add data which will be accessible during event processing but won't get sent to Sentry.
*/
setSDKProcessingMetadata(newData) {
this._sdkProcessingMetadata = merge.merge(this._sdkProcessingMetadata, newData, 2);
return this;
}
/**
* Add propagation context to the scope, used for distributed tracing
*/
setPropagationContext(context) {
this._propagationContext = context;
return this;
}
/**
* Get propagation context from the scope, used for distributed tracing
*/
getPropagationContext() {
return this._propagationContext;
}
/**
* Capture an exception for this scope.
*
* @returns {string} The id of the captured Sentry event.
*/
captureException(exception, hint) {
const eventId = hint?.event_id || misc.uuid4();
if (!this._client) {
logger.logger.warn('No client configured on scope - will not capture exception!');
return eventId;
}
const syntheticException = new Error('Sentry syntheticException');
this._client.captureException(
exception,
{
originalException: exception,
syntheticException,
...hint,
event_id: eventId,
},
this,
);
return eventId;
}
/**
* Capture a message for this scope.
*
* @returns {string} The id of the captured message.
*/
captureMessage(message, level, hint) {
const eventId = hint?.event_id || misc.uuid4();
if (!this._client) {
logger.logger.warn('No client configured on scope - will not capture message!');
return eventId;
}
const syntheticException = new Error(message);
this._client.captureMessage(
message,
level,
{
originalException: message,
syntheticException,
...hint,
event_id: eventId,
},
this,
);
return eventId;
}
/**
* Capture a Sentry event for this scope.
*
* @returns {string} The id of the captured event.
*/
captureEvent(event, hint) {
const eventId = hint?.event_id || misc.uuid4();
if (!this._client) {
logger.logger.warn('No client configured on scope - will not capture event!');
return eventId;
}
this._client.captureEvent(event, { ...hint, event_id: eventId }, this);
return eventId;
}
/**
* 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;
}
}
}
exports.Scope = Scope;
//# sourceMappingURL=scope.js.map