Skip to content

Commit

Permalink
ref(core): Use versioned carrier on global object (#12206)
Browse files Browse the repository at this point in the history
This PR implements a versioned Sentry carrier as described in #12188.
The idea is that SDKs can from now on access their global Sentry
instance and thereby no longer overwrite or interfere with potentially
other SDKs (e.g. 3rd party libraries, scripts, etc).

Internally, SDKs can access their carrier via the
`window.__SENTRY__[SDK_VERSION]`. Externally (spotlight, loader script)
via `window.__SENTRY__[window.__SENTRY__.version]`.

---------

Co-authored-by: Francesco Novy <francesco.novy@sentry.io>
  • Loading branch information
Lms24 and mydea committed May 28, 2024
1 parent fb1ed25 commit 005f40d
Show file tree
Hide file tree
Showing 24 changed files with 411 additions and 90 deletions.
140 changes: 137 additions & 3 deletions dev-packages/browser-integration-tests/fixtures/loader.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,139 @@
!function(n,e,r,t,i,o,a,c,s){for(var u=s,f=0;f<document.scripts.length;f++)if(document.scripts[f].src.indexOf(o)>-1){u&&"no"===document.scripts[f].getAttribute("data-lazy")&&(u=!1);break}var p=[];function l(n){return"e"in n}function d(n){return"p"in n}function _(n){return"f"in n}var v=[];function y(n){u&&(l(n)||d(n)||_(n)&&n.f.indexOf("capture")>-1||_(n)&&n.f.indexOf("showReportDialog")>-1)&&m(),v.push(n)}function g(){y({e:[].slice.call(arguments)})}function h(n){y({p:n})}function E(){try{n.SENTRY_SDK_SOURCE="loader";var e=n[i],o=e.init;e.init=function(i){n.removeEventListener(r,g),n.removeEventListener(t,h);var a=c;for(var s in i)Object.prototype.hasOwnProperty.call(i,s)&&(a[s]=i[s]);!function(n,e){var r=n.integrations||[];if(!Array.isArray(r))return;var t=r.map((function(n){return n.name}));n.tracesSampleRate&&-1===t.indexOf("BrowserTracing")&&(e.BrowserTracing?r.push(new e.BrowserTracing):e.browserTracingIntegration&&r.push(e.browserTracingIntegration()));(n.replaysSessionSampleRate||n.replaysOnErrorSampleRate)&&-1===t.indexOf("Replay")&&(e.Replay?r.push(new e.Replay):e.replayIntegration&&r.push(e.replayIntegration()));n.integrations=r}(a,e),o(a)},setTimeout((function(){return function(e){try{"function"==typeof n.sentryOnLoad&&(n.sentryOnLoad(),n.sentryOnLoad=void 0);for(var r=0;r<p.length;r++)"function"==typeof p[r]&&p[r]();p.splice(0);for(r=0;r<v.length;r++){_(o=v[r])&&"init"===o.f&&e.init.apply(e,o.a)}L()||e.init();var t=n.onerror,i=n.onunhandledrejection;for(r=0;r<v.length;r++){var o;if(_(o=v[r])){if("init"===o.f)continue;e[o.f].apply(e,o.a)}else l(o)&&t?t.apply(n,o.e):d(o)&&i&&i.apply(n,[o.p])}}catch(n){console.error(n)}}(e)}))}catch(n){console.error(n)}}var O=!1;function m(){if(!O){O=!0;var n=e.scripts[0],r=e.createElement("script");r.src=a,r.crossOrigin="anonymous",r.addEventListener("load",E,{once:!0,passive:!0}),n.parentNode.insertBefore(r,n)}}function L(){var e=n.__SENTRY__;return!(void 0===e||!e.hub||!e.hub.getClient())}n[i]=n[i]||{},n[i].onLoad=function(n){L()?n():p.push(n)},n[i].forceLoad=function(){setTimeout((function(){m()}))},["init","addBreadcrumb","captureMessage","captureException","captureEvent","configureScope","withScope","showReportDialog"].forEach((function(e){n[i][e]=function(){y({f:e,a:arguments})}})),n.addEventListener(r,g),n.addEventListener(t,h),u||setTimeout((function(){m()}))}
(
!(function (n, e, r, t, i, o, a, c, s) {
for (var u = s, f = 0; f < document.scripts.length; f++)
if (document.scripts[f].src.indexOf(o) > -1) {
u && 'no' === document.scripts[f].getAttribute('data-lazy') && (u = !1);
break;
}
var p = [];
function l(n) {
return 'e' in n;
}
function d(n) {
return 'p' in n;
}
function _(n) {
return 'f' in n;
}
var v = [];
function y(n) {
u &&
(l(n) || d(n) || (_(n) && n.f.indexOf('capture') > -1) || (_(n) && n.f.indexOf('showReportDialog') > -1)) &&
m(),
v.push(n);
}
function g() {
y({ e: [].slice.call(arguments) });
}
function h(n) {
y({ p: n });
}
function E() {
try {
n.SENTRY_SDK_SOURCE = 'loader';
var e = n[i],
o = e.init;
(e.init = function (i) {
n.removeEventListener(r, g), n.removeEventListener(t, h);
var a = c;
for (var s in i) Object.prototype.hasOwnProperty.call(i, s) && (a[s] = i[s]);
!(function (n, e) {
var r = n.integrations || [];
if (!Array.isArray(r)) return;
var t = r.map(function (n) {
return n.name;
});
n.tracesSampleRate &&
-1 === t.indexOf('BrowserTracing') &&
(e.BrowserTracing
? r.push(new e.BrowserTracing())
: e.browserTracingIntegration && r.push(e.browserTracingIntegration()));
(n.replaysSessionSampleRate || n.replaysOnErrorSampleRate) &&
-1 === t.indexOf('Replay') &&
(e.Replay ? r.push(new e.Replay()) : e.replayIntegration && r.push(e.replayIntegration()));
n.integrations = r;
})(a, e),
o(a);
}),
setTimeout(function () {
return (function (e) {
try {
'function' == typeof n.sentryOnLoad && (n.sentryOnLoad(), (n.sentryOnLoad = void 0));
for (var r = 0; r < p.length; r++) 'function' == typeof p[r] && p[r]();
p.splice(0);
for (r = 0; r < v.length; r++) {
_((o = v[r])) && 'init' === o.f && e.init.apply(e, o.a);
}
L() || e.init();
var t = n.onerror,
i = n.onunhandledrejection;
for (r = 0; r < v.length; r++) {
var o;
if (_((o = v[r]))) {
if ('init' === o.f) continue;
e[o.f].apply(e, o.a);
} else l(o) && t ? t.apply(n, o.e) : d(o) && i && i.apply(n, [o.p]);
}
} catch (n) {
console.error(n);
}
})(e);
});
} catch (n) {
console.error(n);
}
}
var O = !1;
function m() {
if (!O) {
O = !0;
var n = e.scripts[0],
r = e.createElement('script');
(r.src = a),
(r.crossOrigin = 'anonymous'),
r.addEventListener('load', E, { once: !0, passive: !0 }),
n.parentNode.insertBefore(r, n);
}
}
function L() {
var e = n.__SENTRY__;

// TODO: This is a temporary hack to make the loader script compatible with the versioned
// carrier. This needs still needs to be added to the actual loader script before we
// release the loader for v8!
var v = e && e.version && e[e.version];

return !(void 0 === e || !e.hub || !e.hub.getClient()) || !!v;
}
(n[i] = n[i] || {}),
(n[i].onLoad = function (n) {
L() ? n() : p.push(n);
}),
(n[i].forceLoad = function () {
setTimeout(function () {
m();
});
}),
[
'init',
'addBreadcrumb',
'captureMessage',
'captureException',
'captureEvent',
'configureScope',
'withScope',
'showReportDialog',
].forEach(function (e) {
n[i][e] = function () {
y({ f: e, a: arguments });
};
}),
n.addEventListener(r, g),
n.addEventListener(t, h),
u ||
setTimeout(function () {
m();
});
})(
window,
document,
'error',
Expand All @@ -8,5 +142,5 @@
'loader.js',
__LOADER_BUNDLE__,
__LOADER_OPTIONS__,
__LOADER_LAZY__
__LOADER_LAZY__,
);
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,9 @@ window.sentryIsLoaded = () => {
const __sentry = window.__SENTRY__;

// If there is a global __SENTRY__ that means that in any of the callbacks init() was already invoked
return !!(!(typeof __sentry === 'undefined') && __sentry.hub && __sentry.hub.getClient());
return !!(
!(typeof __sentry === 'undefined') &&
__sentry.version &&
!!__sentry[__sentry.version]
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as Sentry from '@sentry/browser';

/**
* This simulates an relatively new v7 SDK setting acs on the __SENTRY__ carrier.
* see: https://github.com/getsentry/sentry-javascript/issues/12054
*/
window.__SENTRY__ = {
acs: {
getCurrentScope: () => {
return 'scope';
},
},
};

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const sentryCarrier = window && window.__SENTRY__;

/**
* Simulate an old pre v8 SDK obtaining the hub from the global sentry carrier
* and checking for the hub version.
*/
const res = sentryCarrier.acs && sentryCarrier.acs.getCurrentScope();

// Write back result into the document
document.getElementById('currentScope').innerText = res && 'scope';
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<p id="currentScope"></p>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { expect } from '@playwright/test';

import { sentryTest } from '../../../../utils/fixtures';

sentryTest(
"doesn't crash if older SDKs access `acs.getCurrentScope` on the global object",
async ({ getLocalTestUrl, page }) => {
const url = await getLocalTestUrl({ testDir: __dirname });
await page.goto(url);

await expect(page.locator('#currentScope')).toHaveText('scope');
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as Sentry from '@sentry/browser';

/**
* This simulates an old, pre-v8 SDK setting itself up on the global __SENTRY__ carrier.
* see: https://github.com/getsentry/sentry-javascript/issues/12155
*/
window.__SENTRY__ = {
hub: {
isOlderThan: version => {
return version < 7;
},
},
};

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const sentryCarrier = window && window.__SENTRY__;

/**
* Simulate an old pre v8 SDK obtaining the hub from the global sentry carrier
* and checking for the hub version.
*/
const res = sentryCarrier.hub && sentryCarrier.hub.isOlderThan(7);

// Write back result into the document
document.getElementById('olderThan').innerText = res;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<p id="olderThan"></p>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { expect } from '@playwright/test';

import { sentryTest } from '../../../../utils/fixtures';

sentryTest(
"doesn't crash if older SDKs access `hub.isOlderThan` on the global object",
async ({ getLocalTestUrl, page }) => {
const url = await getLocalTestUrl({ testDir: __dirname });
await page.goto(url);

await expect(page.locator('#olderThan')).toHaveText('false');
},
);
1 change: 0 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@
"lint": "eslint . --format stylish",
"test": "jest",
"test:watch": "jest --watch",
"version": "node ../../scripts/versionbump.js src/version.ts",
"yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig"
},
"volta": {
Expand Down
20 changes: 5 additions & 15 deletions packages/core/src/asyncContext/stackStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,29 +132,19 @@ export class AsyncContextStack {
*/
function getAsyncContextStack(): AsyncContextStack {
const registry = getMainCarrier();
const sentry = getSentryCarrier(registry);

// For now we continue to keep this as `hub` on the ACS,
// as e.g. the Loader Script relies on this.
// Eventually we may change this if/when we update the loader to not require this field anymore
// Related, we also write to `hub` in {@link ./../sdk.ts registerClientOnGlobalHub}
const sentry = getSentryCarrier(registry) as { hub?: AsyncContextStack };

if (sentry.hub) {
return sentry.hub;
}

sentry.hub = new AsyncContextStack(getDefaultCurrentScope(), getDefaultIsolationScope());
return sentry.hub;
return (sentry.stack = sentry.stack || new AsyncContextStack(getDefaultCurrentScope(), getDefaultIsolationScope()));
}

function withScope<T>(callback: (scope: ScopeInterface) => T): T {
return getAsyncContextStack().withScope(callback);
}

function withSetScope<T>(scope: ScopeInterface, callback: (scope: ScopeInterface) => T): T {
const hub = getAsyncContextStack() as AsyncContextStack;
return hub.withScope(() => {
hub.getStackTop().scope = scope;
const stack = getAsyncContextStack() as AsyncContextStack;
return stack.withScope(() => {
stack.getStackTop().scope = scope;
return callback(scope);
});
}
Expand Down
47 changes: 23 additions & 24 deletions packages/core/src/carrier.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,32 @@
import type { Integration } from '@sentry/types';
import { GLOBAL_OBJ } from '@sentry/utils';
import type { Client, Integration, MetricsAggregator, Scope } from '@sentry/types';
import { GLOBAL_OBJ, SDK_VERSION } from '@sentry/utils';
import type { AsyncContextStack } from './asyncContext/stackStrategy';
import type { AsyncContextStrategy } from './asyncContext/types';

/**
* An object that contains a hub and maintains a scope stack.
* An object that contains globally accessible properties and maintains a scope stack.
* @hidden
*/
export interface Carrier {
__SENTRY__?: SentryCarrier;
__SENTRY__?: VersionedCarrier;
}

type VersionedCarrier = {
version?: string;
} & Record<Exclude<string, 'version'>, SentryCarrier>;

interface SentryCarrier {
acs?: AsyncContextStrategy;
}
stack?: AsyncContextStack;

/**
* An object that contains a hub and maintains a scope stack.
* @hidden
*/
export interface Carrier {
__SENTRY__?: SentryCarrier;
}
globalScope?: Scope;
defaultIsolationScope?: Scope;
defaultCurrentScope?: Scope;
globalMetricsAggregators?: WeakMap<Client, MetricsAggregator> | undefined;

interface SentryCarrier {
acs?: AsyncContextStrategy;
/**
* Extra Hub properties injected by various SDKs
*/
// TODO(v9): Remove these properties - they are no longer used and were left over in v8
integrations?: Integration[];
extensions?: {
/** Extension methods for the hub, which are bound to the current Hub instance */
// eslint-disable-next-line @typescript-eslint/ban-types
[key: string]: Function;
};
Expand All @@ -50,10 +47,12 @@ export function getMainCarrier(): Carrier {

/** Will either get the existing sentry carrier, or create a new one. */
export function getSentryCarrier(carrier: Carrier): SentryCarrier {
if (!carrier.__SENTRY__) {
carrier.__SENTRY__ = {
extensions: {},
};
}
return carrier.__SENTRY__;
const __SENTRY__ = (carrier.__SENTRY__ = carrier.__SENTRY__ || {});

// For now: First SDK that sets the .version property wins
__SENTRY__.version = __SENTRY__.version || SDK_VERSION;

// Intentionally populating and returning the version of "this" SDK instance
// rather than what's set in .version so that "this" SDK always gets its carrier
return (__SENTRY__[SDK_VERSION] = __SENTRY__[SDK_VERSION] || {});
}
3 changes: 2 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ export { initAndBind, setCurrentClient } from './sdk';
export { createTransport } from './transports/base';
export { makeOfflineTransport } from './transports/offline';
export { makeMultiplexedTransport } from './transports/multiplexed';
export { SDK_VERSION } from './version';
export {
getIntegrationsToSetup,
addIntegration,
Expand Down Expand Up @@ -107,3 +106,5 @@ export { captureFeedback } from './feedback';

// eslint-disable-next-line deprecation/deprecation
export { getCurrentHubShim, getCurrentHub } from './getCurrentHubShim';

export { SDK_VERSION } from '@sentry/utils';
Loading

0 comments on commit 005f40d

Please sign in to comment.