Skip to content

Commit e2ddba2

Browse files
mydeaLuca Forstner
andauthored
fix(loader): Improve loader callback handling (#58070)
This slightly refactors the loader script and clarifies function/variable naming etc. --------- Co-authored-by: Luca Forstner <[email protected]>
1 parent 04ebf82 commit e2ddba2

File tree

3 files changed

+234
-197
lines changed

3 files changed

+234
-197
lines changed
Lines changed: 104 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
{% load sentry_helpers %}(function sentryLoader(_window, _document, _errorEvent, _unhandledrejectionEvent, _namespace, _publicKey, _sdkBundleUrl, _config, _lazy) {
1+
{% load sentry_helpers %}(function sentryLoader(_window, _document, _errorEvent, _unhandledrejectionEvent, _namespace, _publicKey, _sdkBundleUrl, _loaderInitConfig, _lazy) {
22
var lazy = _lazy;
3-
var forceLoad = false;
43
for (var i = 0; i < document.scripts.length; i++) {
54
if (document.scripts[i].src.indexOf(_publicKey) > -1) {
65
// If lazy was set to true above, we need to check if the user has set data-lazy="no"
@@ -11,84 +10,100 @@
1110
break;
1211
}
1312
}
14-
var injected = false;
1513
var onLoadCallbacks = [];
14+
function queueIsError(item) {
15+
return 'e' in item;
16+
}
17+
function queueIsPromiseRejection(item) {
18+
return 'p' in item;
19+
}
20+
function queueIsFunction(item) {
21+
return 'f' in item;
22+
}
23+
var queue = [];
1624
// Create a namespace and attach function that will store captured exception
1725
// Because functions are also objects, we can attach the queue itself straight to it and save some bytes
18-
var queue = function (content) {
19-
// content.e = error
20-
// content.p = promise rejection
21-
// content.f = function call the Sentry
22-
if (('e' in content ||
23-
'p' in content ||
24-
(content.f && content.f.indexOf('capture') > -1) ||
25-
(content.f && content.f.indexOf('showReportDialog') > -1)) &&
26-
lazy) {
26+
function enqueue(item) {
27+
if (lazy &&
28+
(queueIsError(item) ||
29+
queueIsPromiseRejection(item) ||
30+
(queueIsFunction(item) && item.f.indexOf('capture') > -1) ||
31+
(queueIsFunction(item) && item.f.indexOf('showReportDialog') > -1))) {
2732
// We only want to lazy inject/load the sdk bundle if
2833
// an error or promise rejection occured
2934
// OR someone called `capture...` on the SDK
30-
injectSdk(onLoadCallbacks);
35+
injectCDNScriptTag();
3136
}
32-
queue.data.push(content);
33-
};
34-
queue.data = [];
37+
queue.push(item);
38+
}
3539
function onError() {
3640
// Use keys as "data type" to save some characters"
37-
queue({
41+
enqueue({
3842
e: [].slice.call(arguments),
3943
});
4044
}
4145
function onUnhandledRejection(e) {
42-
queue({
46+
enqueue({
4347
p: 'reason' in e
4448
? e.reason
4549
: 'detail' in e && 'reason' in e.detail
4650
? e.detail.reason
4751
: e,
4852
});
4953
}
50-
function injectSdk(callbacks) {
51-
if (injected) {
54+
function onSentryCDNScriptLoaded() {
55+
try {
56+
// Add loader as SDK source
57+
_window.SENTRY_SDK_SOURCE = 'loader';
58+
var SDK_1 = _window[_namespace];
59+
var cdnInit_1 = SDK_1.init;
60+
// Configure it using provided DSN and config object
61+
SDK_1.init = function (options) {
62+
// Remove the lazy mode error event listeners that we previously registered
63+
// Once we call init, we can assume that Sentry has added it's own global error listeners
64+
_window.removeEventListener(_errorEvent, onError);
65+
_window.removeEventListener(_unhandledrejectionEvent, onUnhandledRejection);
66+
var mergedInitOptions = _loaderInitConfig;
67+
for (var key in options) {
68+
if (Object.prototype.hasOwnProperty.call(options, key)) {
69+
mergedInitOptions[key] = options[key];
70+
}
71+
}
72+
setupDefaultIntegrations(mergedInitOptions, SDK_1);
73+
cdnInit_1(mergedInitOptions);
74+
};
75+
// Wait a tick to ensure that all `Sentry.onLoad()` callbacks have been registered
76+
setTimeout(function () { return setupSDK(SDK_1); });
77+
}
78+
catch (o_O) {
79+
console.error(o_O);
80+
}
81+
}
82+
var injectedCDNScriptTag = false;
83+
/**
84+
* Injects script tag into the page pointing to the CDN bundle.
85+
*/
86+
function injectCDNScriptTag() {
87+
if (injectedCDNScriptTag) {
5288
return;
5389
}
54-
injected = true;
90+
injectedCDNScriptTag = true;
5591
// Create a `script` tag with provided SDK `url` and attach it just before the first, already existing `script` tag
5692
// Scripts that are dynamically created and added to the document are async by default,
5793
// they don't block rendering and execute as soon as they download, meaning they could
5894
// come out in the wrong order. Because of that we don't need async=1 as GA does.
5995
// it was probably(?) a legacy behavior that they left to not modify few years old snippet
6096
// https://www.html5rocks.com/en/tutorials/speed/script-loading/
61-
var _currentScriptTag = _document.scripts[0];
62-
var _newScriptTag = _document.createElement('script');
63-
_newScriptTag.src = _sdkBundleUrl;
64-
_newScriptTag.crossOrigin = 'anonymous';
97+
var firstScriptTagInDom = _document.scripts[0];
98+
var cdnScriptTag = _document.createElement('script');
99+
cdnScriptTag.src = _sdkBundleUrl;
100+
cdnScriptTag.crossOrigin = 'anonymous';
65101
// Once our SDK is loaded
66-
_newScriptTag.addEventListener('load', function () {
67-
try {
68-
_window.removeEventListener(_errorEvent, onError);
69-
_window.removeEventListener(_unhandledrejectionEvent, onUnhandledRejection);
70-
// Add loader as SDK source
71-
_window.SENTRY_SDK_SOURCE = 'loader';
72-
var SDK_1 = _window[_namespace];
73-
var oldInit_1 = SDK_1.init;
74-
// Configure it using provided DSN and config object
75-
SDK_1.init = function (options) {
76-
var target = _config;
77-
for (var key in options) {
78-
if (Object.prototype.hasOwnProperty.call(options, key)) {
79-
target[key] = options[key];
80-
}
81-
}
82-
setupDefaultIntegrations(target, SDK_1);
83-
oldInit_1(target);
84-
};
85-
sdkLoaded(callbacks, SDK_1);
86-
}
87-
catch (o_O) {
88-
console.error(o_O);
89-
}
102+
cdnScriptTag.addEventListener('load', onSentryCDNScriptLoaded, {
103+
once: true,
104+
passive: true,
90105
});
91-
_currentScriptTag.parentNode.insertBefore(_newScriptTag, _currentScriptTag);
106+
firstScriptTagInDom.parentNode.insertBefore(cdnScriptTag, firstScriptTagInDom);
92107
}
93108
// We want to ensure to only add default integrations if they haven't been added by the user.
94109
function setupDefaultIntegrations(config, SDK) {
@@ -115,48 +130,46 @@
115130
__sentry.hub &&
116131
__sentry.hub.getClient());
117132
}
118-
function sdkLoaded(callbacks, SDK) {
133+
function setupSDK(SDK) {
119134
try {
120135
// We have to make sure to call all callbacks first
121-
for (var i = 0; i < callbacks.length; i++) {
122-
if (typeof callbacks[i] === 'function') {
123-
callbacks[i]();
136+
for (var i = 0; i < onLoadCallbacks.length; i++) {
137+
if (typeof onLoadCallbacks[i] === 'function') {
138+
onLoadCallbacks[i]();
124139
}
125140
}
126-
var data = queue.data;
127-
var initAlreadyCalled = sdkIsLoaded();
128-
// Call init first, if provided
129-
data.sort(function (a) { return (a.f === 'init' ? -1 : 0); });
130-
// We want to replay all calls to Sentry and also make sure that `init` is called if it wasn't already
131-
// We replay all calls to `Sentry.*` now
132-
var calledSentry = false;
133-
for (var i = 0; i < data.length; i++) {
134-
if (data[i].f) {
135-
calledSentry = true;
136-
var call = data[i];
137-
if (initAlreadyCalled === false && call.f !== 'init') {
138-
// First call always has to be init, this is a conveniece for the user so call to init is optional
139-
SDK.init();
140-
}
141-
initAlreadyCalled = true;
142-
SDK[call.f].apply(SDK, call.a);
141+
// First call all inits from the queue
142+
for (var i = 0; i < queue.length; i++) {
143+
var item = queue[i];
144+
if (queueIsFunction(item) && item.f === 'init') {
145+
SDK.init.apply(SDK, item.a);
143146
}
144147
}
145-
if (initAlreadyCalled === false && calledSentry === false) {
146-
// Sentry has never been called but we need Sentry.init() so call it
148+
// If the SDK has not been called manually, either in an onLoad callback, or somewhere else,
149+
// we initialize it for the user
150+
if (!sdkIsLoaded()) {
147151
SDK.init();
148152
}
149-
// Because we installed the SDK, at this point we have an access to TraceKit's handler,
153+
// Now, we _know_ that the SDK is initialized, and can continue with the rest of the queue
154+
// Because we installed the SDK, at this point we can assume that the global handlers have been patched
150155
// which can take care of browser differences (eg. missing exception argument in onerror)
151-
var tracekitErrorHandler = _window.onerror;
152-
var tracekitUnhandledRejectionHandler = _window.onunhandledrejection;
153-
// And now capture all previously caught exceptions
154-
for (var i = 0; i < data.length; i++) {
155-
if ('e' in data[i] && tracekitErrorHandler) {
156-
tracekitErrorHandler.apply(_window, data[i].e);
156+
var sentryPatchedErrorHandler = _window.onerror;
157+
var sentryPatchedUnhandledRejectionHandler = _window.onunhandledrejection;
158+
for (var i = 0; i < queue.length; i++) {
159+
var item = queue[i];
160+
if (queueIsFunction(item)) {
161+
// We already called all init before, so just skip this
162+
if (item.f === 'init') {
163+
continue;
164+
}
165+
SDK[item.f].apply(SDK, item.a);
166+
}
167+
else if (queueIsError(item) && sentryPatchedErrorHandler) {
168+
sentryPatchedErrorHandler.apply(_window, item.e);
157169
}
158-
else if ('p' in data[i] && tracekitUnhandledRejectionHandler) {
159-
tracekitUnhandledRejectionHandler.apply(_window, [data[i].p]);
170+
else if (queueIsPromiseRejection(item) &&
171+
sentryPatchedUnhandledRejectionHandler) {
172+
sentryPatchedUnhandledRejectionHandler.apply(_window, [item.p]);
160173
}
161174
}
162175
}
@@ -167,19 +180,17 @@
167180
// We make sure we do not overwrite window.Sentry since there could be already integrations in there
168181
_window[_namespace] = _window[_namespace] || {};
169182
_window[_namespace].onLoad = function (callback) {
170-
onLoadCallbacks.push(callback);
171-
if (lazy && !forceLoad) {
183+
// If the SDK was already loaded, call the callback immediately
184+
if (sdkIsLoaded()) {
185+
callback();
172186
return;
173187
}
174-
injectSdk(onLoadCallbacks);
188+
onLoadCallbacks.push(callback);
175189
};
176190
_window[_namespace].forceLoad = function () {
177-
forceLoad = true;
178-
if (lazy) {
179-
setTimeout(function () {
180-
injectSdk(onLoadCallbacks);
181-
});
182-
}
191+
setTimeout(function () {
192+
injectCDNScriptTag();
193+
});
183194
};
184195
[
185196
'init',
@@ -192,14 +203,14 @@
192203
'showReportDialog',
193204
].forEach(function (f) {
194205
_window[_namespace][f] = function () {
195-
queue({ f: f, a: arguments });
206+
enqueue({ f: f, a: arguments });
196207
};
197208
});
198209
_window.addEventListener(_errorEvent, onError);
199210
_window.addEventListener(_unhandledrejectionEvent, onUnhandledRejection);
200211
if (!lazy) {
201212
setTimeout(function () {
202-
injectSdk(onLoadCallbacks);
213+
injectCDNScriptTag();
203214
});
204215
}
205216
})(window, document, 'error', 'unhandledrejection', 'Sentry', '{{ publicKey|safe }}', '{{ jsSdkUrl|safe }}', {{ config|to_json|safe }}, {{ isLazy|safe|lower }});
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{% load sentry_helpers %}!function(e,n,r,t,i,o,a,c,s){for(var f=s,forceLoad=!1,u=0;u<document.scripts.length;u++)if(document.scripts[u].src.indexOf(o)>-1){f&&"no"===document.scripts[u].getAttribute("data-lazy")&&(f=!1);break}var p=!1,d=[],l=function(e){("e"in e||"p"in e||e.f&&e.f.indexOf("capture")>-1||e.f&&e.f.indexOf("showReportDialog")>-1)&&f&&h(d),l.data.push(e)};function _(){l({e:[].slice.call(arguments)})}function v(e){l({p:"reason"in e?e.reason:"detail"in e&&"reason"in e.detail?e.detail.reason:e})}function h(o){if(!p){p=!0;var s=n.scripts[0],f=n.createElement("script");f.src=a,f.crossOrigin="anonymous",f.addEventListener("load",(function(){try{e.removeEventListener(r,_),e.removeEventListener(t,v),e.SENTRY_SDK_SOURCE="loader";var n=e[i],a=n.init;n.init=function(e){var r=c;for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&(r[t]=e[t]);!function(e,n){var r=e.integrations||[];if(!Array.isArray(r))return;var t=r.map((function(e){return e.name}));e.tracesSampleRate&&-1===t.indexOf("BrowserTracing")&&r.push(new n.BrowserTracing);(e.replaysSessionSampleRate||e.replaysOnErrorSampleRate)&&-1===t.indexOf("Replay")&&r.push(new n.Replay);e.integrations=r}(r,n),a(r)},function(n,r){try{for(var t=0;t<n.length;t++)"function"==typeof n[t]&&n[t]();var i=l.data,o=!(void 0===(u=e.__SENTRY__)||!u.hub||!u.hub.getClient());i.sort((function(e){return"init"===e.f?-1:0}));var a=!1;for(t=0;t<i.length;t++)if(i[t].f){a=!0;var c=i[t];!1===o&&"init"!==c.f&&r.init(),o=!0,r[c.f].apply(r,c.a)}!1===o&&!1===a&&r.init();var s=e.onerror,f=e.onunhandledrejection;for(t=0;t<i.length;t++)"e"in i[t]&&s?s.apply(e,i[t].e):"p"in i[t]&&f&&f.apply(e,[i[t].p])}catch(e){console.error(e)}var u}(o,n)}catch(e){console.error(e)}})),s.parentNode.insertBefore(f,s)}}l.data=[],e[i]=e[i]||{},e[i].onLoad=function(e){d.push(e),f&&!forceLoad||h(d)},e[i].forceLoad=function(){forceLoad=!0,f&&setTimeout((function(){h(d)}))},["init","addBreadcrumb","captureMessage","captureException","captureEvent","configureScope","withScope","showReportDialog"].forEach((function(n){e[i][n]=function(){l({f:n,a:arguments})}})),e.addEventListener(r,_),e.addEventListener(t,v),f||setTimeout((function(){h(d)}))}(window,document,"error","unhandledrejection","Sentry",'{{ publicKey|safe }}','{{ jsSdkUrl|safe }}',{{ config|to_json|safe }},{{ isLazy|safe|lower }});
1+
{% load sentry_helpers %}!function(n,e,r,t,i,o,a,c,u){for(var s=u,f=0;f<document.scripts.length;f++)if(document.scripts[f].src.indexOf(o)>-1){s&&"no"===document.scripts[f].getAttribute("data-lazy")&&(s=!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 h(n){s&&(l(n)||d(n)||_(n)&&n.f.indexOf("capture")>-1||_(n)&&n.f.indexOf("showReportDialog")>-1)&&O(),v.push(n)}function y(){h({e:[].slice.call(arguments)})}function E(n){h({p:"reason"in n?n.reason:"detail"in n&&"reason"in n.detail?n.detail.reason:n})}function m(){try{n.SENTRY_SDK_SOURCE="loader";var e=n[i],o=e.init;e.init=function(i){n.removeEventListener(r,y),n.removeEventListener(t,E);var a=c;for(var u in i)Object.prototype.hasOwnProperty.call(i,u)&&(a[u]=i[u]);!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")&&r.push(new e.BrowserTracing);(n.replaysSessionSampleRate||n.replaysOnErrorSampleRate)&&-1===t.indexOf("Replay")&&r.push(new e.Replay);n.integrations=r}(a,e),o(a)},setTimeout((function(){return function(e){try{for(var r=0;r<p.length;r++)"function"==typeof p[r]&&p[r]();for(r=0;r<v.length;r++){_(o=v[r])&&"init"===o.f&&e.init.apply(e,o.a)}R()||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 g=!1;function O(){if(!g){g=!0;var n=e.scripts[0],r=e.createElement("script");r.src=a,r.crossOrigin="anonymous",r.addEventListener("load",m,{once:!0,passive:!0}),n.parentNode.insertBefore(r,n)}}function R(){var e=n.__SENTRY__;return!(void 0===e||!e.hub||!e.hub.getClient())}n[i]=n[i]||{},n[i].onLoad=function(n){R()?n():p.push(n)},n[i].forceLoad=function(){setTimeout((function(){O()}))},["init","addBreadcrumb","captureMessage","captureException","captureEvent","configureScope","withScope","showReportDialog"].forEach((function(e){n[i][e]=function(){h({f:e,a:arguments})}})),n.addEventListener(r,y),n.addEventListener(t,E),s||setTimeout((function(){O()}))}(window,document,"error","unhandledrejection","Sentry",'{{ publicKey|safe }}','{{ jsSdkUrl|safe }}',{{ config|to_json|safe }},{{ isLazy|safe|lower }});

0 commit comments

Comments
 (0)