@sentry/core
Version:
Base implementation for all Sentry JavaScript SDKs
550 lines (482 loc) • 13.7 kB
JavaScript
import { uuid4, dateTimestampInSeconds, consoleSandbox, logger, GLOBAL_OBJ, isNodeEnv, getGlobalSingleton } from '@sentry/utils';
import { DEFAULT_ENVIRONMENT } from './constants.js';
import { Scope } from './scope.js';
import { closeSession, makeSession, updateSession } from './session.js';
/**
* API compatibility version of this hub.
*
* WARNING: This number should only be increased when the global interface
* changes and new methods are introduced.
*
* @hidden
*/
const API_VERSION = 4;
/**
* Default maximum number of breadcrumbs added to an event. Can be overwritten
* with {@link Options.maxBreadcrumbs}.
*/
const DEFAULT_BREADCRUMBS = 100;
/**
* A layer in the process stack.
* @hidden
*/
/**
* @inheritDoc
*/
class Hub {
/** Is a {@link Layer}[] containing the client and scope */
__init() {this._stack = [{}];}
/** Contains the last event id of a captured event. */
/**
* Creates a new instance of the hub, will push one {@link Layer} into the
* internal stack on creation.
*
* @param client bound to the hub.
* @param scope bound to the hub.
* @param version number, higher number means higher priority.
*/
constructor(client, scope = new Scope(), _version = API_VERSION) {this._version = _version;Hub.prototype.__init.call(this);
this.getStackTop().scope = scope;
if (client) {
this.bindClient(client);
}
}
/**
* @inheritDoc
*/
isOlderThan(version) {
return this._version < version;
}
/**
* @inheritDoc
*/
bindClient(client) {
const top = this.getStackTop();
top.client = client;
if (client && client.setupIntegrations) {
client.setupIntegrations();
}
}
/**
* @inheritDoc
*/
pushScope() {
// We want to clone the content of prev scope
const scope = Scope.clone(this.getScope());
this.getStack().push({
client: this.getClient(),
scope,
});
return scope;
}
/**
* @inheritDoc
*/
popScope() {
if (this.getStack().length <= 1) return false;
return !!this.getStack().pop();
}
/**
* @inheritDoc
*/
withScope(callback) {
const scope = this.pushScope();
try {
callback(scope);
} finally {
this.popScope();
}
}
/**
* @inheritDoc
*/
getClient() {
return this.getStackTop().client ;
}
/** Returns the scope of the top stack. */
getScope() {
return this.getStackTop().scope;
}
/** Returns the scope stack for domains or the process. */
getStack() {
return this._stack;
}
/** Returns the topmost scope layer in the order domain > local > process. */
getStackTop() {
return this._stack[this._stack.length - 1];
}
/**
* @inheritDoc
*/
captureException(exception, hint) {
const eventId = (this._lastEventId = hint && hint.event_id ? hint.event_id : uuid4());
const syntheticException = new Error('Sentry syntheticException');
this._withClient((client, scope) => {
client.captureException(
exception,
{
originalException: exception,
syntheticException,
...hint,
event_id: eventId,
},
scope,
);
});
return eventId;
}
/**
* @inheritDoc
*/
captureMessage(
message,
// eslint-disable-next-line deprecation/deprecation
level,
hint,
) {
const eventId = (this._lastEventId = hint && hint.event_id ? hint.event_id : uuid4());
const syntheticException = new Error(message);
this._withClient((client, scope) => {
client.captureMessage(
message,
level,
{
originalException: message,
syntheticException,
...hint,
event_id: eventId,
},
scope,
);
});
return eventId;
}
/**
* @inheritDoc
*/
captureEvent(event, hint) {
const eventId = hint && hint.event_id ? hint.event_id : uuid4();
if (!event.type) {
this._lastEventId = eventId;
}
this._withClient((client, scope) => {
client.captureEvent(event, { ...hint, event_id: eventId }, scope);
});
return eventId;
}
/**
* @inheritDoc
*/
lastEventId() {
return this._lastEventId;
}
/**
* @inheritDoc
*/
addBreadcrumb(breadcrumb, hint) {
const { scope, client } = this.getStackTop();
if (!scope || !client) return;
const { beforeBreadcrumb = null, maxBreadcrumbs = DEFAULT_BREADCRUMBS } =
(client.getOptions && client.getOptions()) || {};
if (maxBreadcrumbs <= 0) return;
const timestamp = dateTimestampInSeconds();
const mergedBreadcrumb = { timestamp, ...breadcrumb };
const finalBreadcrumb = beforeBreadcrumb
? (consoleSandbox(() => beforeBreadcrumb(mergedBreadcrumb, hint)) )
: mergedBreadcrumb;
if (finalBreadcrumb === null) return;
if (client.emit) {
client.emit('beforeAddBreadcrumb', finalBreadcrumb, hint);
}
scope.addBreadcrumb(finalBreadcrumb, maxBreadcrumbs);
}
/**
* @inheritDoc
*/
setUser(user) {
const scope = this.getScope();
if (scope) scope.setUser(user);
}
/**
* @inheritDoc
*/
setTags(tags) {
const scope = this.getScope();
if (scope) scope.setTags(tags);
}
/**
* @inheritDoc
*/
setExtras(extras) {
const scope = this.getScope();
if (scope) scope.setExtras(extras);
}
/**
* @inheritDoc
*/
setTag(key, value) {
const scope = this.getScope();
if (scope) scope.setTag(key, value);
}
/**
* @inheritDoc
*/
setExtra(key, extra) {
const scope = this.getScope();
if (scope) scope.setExtra(key, extra);
}
/**
* @inheritDoc
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setContext(name, context) {
const scope = this.getScope();
if (scope) scope.setContext(name, context);
}
/**
* @inheritDoc
*/
configureScope(callback) {
const { scope, client } = this.getStackTop();
if (scope && client) {
callback(scope);
}
}
/**
* @inheritDoc
*/
run(callback) {
const oldHub = makeMain(this);
try {
callback(this);
} finally {
makeMain(oldHub);
}
}
/**
* @inheritDoc
*/
getIntegration(integration) {
const client = this.getClient();
if (!client) return null;
try {
return client.getIntegration(integration);
} catch (_oO) {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.warn(`Cannot retrieve integration ${integration.id} from the current Hub`);
return null;
}
}
/**
* @inheritDoc
*/
startTransaction(context, customSamplingContext) {
return this._callExtensionMethod('startTransaction', context, customSamplingContext);
}
/**
* @inheritDoc
*/
traceHeaders() {
return this._callExtensionMethod('traceHeaders');
}
/**
* @inheritDoc
*/
captureSession(endSession = false) {
// both send the update and pull the session from the scope
if (endSession) {
return this.endSession();
}
// only send the update
this._sendSessionUpdate();
}
/**
* @inheritDoc
*/
endSession() {
const layer = this.getStackTop();
const scope = layer && layer.scope;
const session = scope && scope.getSession();
if (session) {
closeSession(session);
}
this._sendSessionUpdate();
// the session is over; take it off of the scope
if (scope) {
scope.setSession();
}
}
/**
* @inheritDoc
*/
startSession(context) {
const { scope, client } = this.getStackTop();
const { release, environment = DEFAULT_ENVIRONMENT } = (client && client.getOptions()) || {};
// Will fetch userAgent if called from browser sdk
const { userAgent } = GLOBAL_OBJ.navigator || {};
const session = makeSession({
release,
environment,
...(scope && { user: scope.getUser() }),
...(userAgent && { userAgent }),
...context,
});
if (scope) {
// End existing session if there's one
const currentSession = scope.getSession && scope.getSession();
if (currentSession && currentSession.status === 'ok') {
updateSession(currentSession, { status: 'exited' });
}
this.endSession();
// Afterwards we set the new session on the scope
scope.setSession(session);
}
return session;
}
/**
* Returns if default PII should be sent to Sentry and propagated in ourgoing requests
* when Tracing is used.
*/
shouldSendDefaultPii() {
const client = this.getClient();
const options = client && client.getOptions();
return Boolean(options && options.sendDefaultPii);
}
/**
* Sends the current Session on the scope
*/
_sendSessionUpdate() {
const { scope, client } = this.getStackTop();
if (!scope) return;
const session = scope.getSession();
if (session) {
if (client && client.captureSession) {
client.captureSession(session);
}
}
}
/**
* Internal helper function to call a method on the top client if it exists.
*
* @param method The method to call on the client.
* @param args Arguments to pass to the client function.
*/
_withClient(callback) {
const { scope, client } = this.getStackTop();
if (client) {
callback(client, scope);
}
}
/**
* Calls global extension method and binding current instance to the function call
*/
// @ts-ignore Function lacks ending return statement and return type does not include 'undefined'. ts(2366)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_callExtensionMethod(method, ...args) {
const carrier = getMainCarrier();
const sentry = carrier.__SENTRY__;
if (sentry && sentry.extensions && typeof sentry.extensions[method] === 'function') {
return sentry.extensions[method].apply(this, args);
}
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.warn(`Extension method ${method} couldn't be found, doing nothing.`);
}
}
/**
* Returns the global shim registry.
*
* FIXME: This function is problematic, because despite always returning a valid Carrier,
* it has an optional `__SENTRY__` property, which then in turn requires us to always perform an unnecessary check
* at the call-site. We always access the carrier through this function, so we can guarantee that `__SENTRY__` is there.
**/
function getMainCarrier() {
GLOBAL_OBJ.__SENTRY__ = GLOBAL_OBJ.__SENTRY__ || {
extensions: {},
hub: undefined,
};
return GLOBAL_OBJ;
}
/**
* Replaces the current main hub with the passed one on the global object
*
* @returns The old replaced hub
*/
function makeMain(hub) {
const registry = getMainCarrier();
const oldHub = getHubFromCarrier(registry);
setHubOnCarrier(registry, hub);
return oldHub;
}
/**
* Returns the default hub instance.
*
* If a hub is already registered in the global carrier but this module
* contains a more recent version, it replaces the registered version.
* Otherwise, the currently registered hub will be returned.
*/
function getCurrentHub() {
// Get main carrier (global for every environment)
const registry = getMainCarrier();
// If there's no hub, or its an old API, assign a new one
if (!hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(API_VERSION)) {
setHubOnCarrier(registry, new Hub());
}
// Prefer domains over global if they are there (applicable only to Node environment)
if (isNodeEnv()) {
return getHubFromActiveDomain(registry);
}
// Return hub that lives on a global object
return getHubFromCarrier(registry);
}
/**
* Try to read the hub from an active domain, and fallback to the registry if one doesn't exist
* @returns discovered hub
*/
function getHubFromActiveDomain(registry) {
try {
const sentry = getMainCarrier().__SENTRY__;
const activeDomain = sentry && sentry.extensions && sentry.extensions.domain && sentry.extensions.domain.active;
// If there's no active domain, just return global hub
if (!activeDomain) {
return getHubFromCarrier(registry);
}
// If there's no hub on current domain, or it's an old API, assign a new one
if (!hasHubOnCarrier(activeDomain) || getHubFromCarrier(activeDomain).isOlderThan(API_VERSION)) {
const registryHubTopStack = getHubFromCarrier(registry).getStackTop();
setHubOnCarrier(activeDomain, new Hub(registryHubTopStack.client, Scope.clone(registryHubTopStack.scope)));
}
// Return hub that lives on a domain
return getHubFromCarrier(activeDomain);
} catch (_Oo) {
// Return hub that lives on a global object
return getHubFromCarrier(registry);
}
}
/**
* This will tell whether a carrier has a hub on it or not
* @param carrier object
*/
function hasHubOnCarrier(carrier) {
return !!(carrier && carrier.__SENTRY__ && carrier.__SENTRY__.hub);
}
/**
* This will create a new {@link Hub} and add to the passed object on
* __SENTRY__.hub.
* @param carrier object
* @hidden
*/
function getHubFromCarrier(carrier) {
return getGlobalSingleton('hub', () => new Hub(), carrier);
}
/**
* This will set passed {@link Hub} on the passed object's __SENTRY__.hub attribute
* @param carrier object
* @param hub Hub
* @returns A boolean indicating success or failure
*/
function setHubOnCarrier(carrier, hub) {
if (!carrier) return false;
const __SENTRY__ = (carrier.__SENTRY__ = carrier.__SENTRY__ || {});
__SENTRY__.hub = hub;
return true;
}
export { API_VERSION, Hub, getCurrentHub, getHubFromCarrier, getMainCarrier, makeMain, setHubOnCarrier };
//# sourceMappingURL=hub.js.map