Skip to content

Commit 026a554

Browse files
committed
test(loader): Add integration tests for JS loader behavior
fix loader
1 parent 79ca4a7 commit 026a554

File tree

35 files changed

+781
-57
lines changed

35 files changed

+781
-57
lines changed

.github/workflows/build.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,59 @@ jobs:
543543
cd packages/browser-integration-tests
544544
yarn test:ci
545545
546+
job_browser_loader_tests:
547+
name: Playwright Loader (${{ matrix.bundle }}) Tests
548+
needs: [job_get_metadata, job_build]
549+
if: needs.job_get_metadata.outputs.changed_browser_integration == 'true' || github.event_name != 'pull_request'
550+
runs-on: ubuntu-20.04
551+
timeout-minutes: 15
552+
strategy:
553+
fail-fast: false
554+
matrix:
555+
bundle:
556+
- loader_base
557+
- loader_eager
558+
- loader_tracing
559+
- loader_full
560+
561+
steps:
562+
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
563+
uses: actions/checkout@v3
564+
with:
565+
ref: ${{ env.HEAD_COMMIT }}
566+
- name: Set up Node
567+
uses: volta-cli/action@v4
568+
- name: Restore caches
569+
uses: ./.github/actions/restore-cache
570+
env:
571+
DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }}
572+
- name: Get npm cache directory
573+
id: npm-cache-dir
574+
run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
575+
- name: Get Playwright version
576+
id: playwright-version
577+
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT
578+
- uses: actions/cache@v3
579+
name: Check if Playwright browser is cached
580+
id: playwright-cache
581+
with:
582+
path: ${{ steps.npm-cache-dir.outputs.dir }}
583+
key: ${{ runner.os }}-Playwright-${{steps.playwright-version.outputs.version}}
584+
- name: Install Playwright browser if not cached
585+
if: steps.playwright-cache.outputs.cache-hit != 'true'
586+
run: npx playwright install --with-deps
587+
env:
588+
PLAYWRIGHT_BROWSERS_PATH: ${{steps.npm-cache-dir.outputs.dir}}
589+
- name: Install OS dependencies of Playwright if cache hit
590+
if: steps.playwright-cache.outputs.cache-hit == 'true'
591+
run: npx playwright install-deps
592+
- name: Run Playwright Loader tests
593+
env:
594+
PW_BUNDLE: ${{ matrix.bundle }}
595+
run: |
596+
cd packages/browser-integration-tests
597+
yarn test:loader
598+
546599
job_browser_integration_tests:
547600
name: Browser (${{ matrix.browser }}) Tests
548601
needs: [job_get_metadata, job_build]

packages/browser-integration-tests/.eslintrc.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ module.exports = {
44
node: true,
55
},
66
extends: ['../../.eslintrc.js'],
7-
ignorePatterns: ['suites/**/subject.js', 'suites/**/dist/*', 'scripts/**'],
7+
ignorePatterns: [
8+
'suites/**/subject.js',
9+
'suites/**/dist/*',
10+
'loader-suites/**/dist/*',
11+
'loader-suites/**/subject.js',
12+
'scripts/**',
13+
],
814
parserOptions: {
915
sourceType: 'module',
1016
},
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
/* eslint-disable */
2+
// prettier-ignore
3+
// Prettier disabled due to trailing comma not working in IE10/11
4+
(function(
5+
_window,
6+
_document,
7+
_script,
8+
_onerror,
9+
_onunhandledrejection,
10+
_namespace,
11+
_publicKey,
12+
_sdkBundleUrl,
13+
_config,
14+
_lazy
15+
) {
16+
var lazy = _lazy;
17+
var forceLoad = false;
18+
19+
for (var i = 0; i < document.scripts.length; i++) {
20+
if (document.scripts[i].src.indexOf(_publicKey) > -1) {
21+
// If lazy was set to true above, we need to check if the user has set data-lazy="no"
22+
// to confirm that we should lazy load the CDN bundle
23+
if (lazy && document.scripts[i].getAttribute('data-lazy') === 'no') {
24+
lazy = false;
25+
}
26+
break;
27+
}
28+
}
29+
30+
var injected = false;
31+
var onLoadCallbacks = [];
32+
33+
// Create a namespace and attach function that will store captured exception
34+
// Because functions are also objects, we can attach the queue itself straight to it and save some bytes
35+
var queue = function(content) {
36+
// content.e = error
37+
// content.p = promise rejection
38+
// content.f = function call the Sentry
39+
if (
40+
('e' in content ||
41+
'p' in content ||
42+
(content.f && content.f.indexOf('capture') > -1) ||
43+
(content.f && content.f.indexOf('showReportDialog') > -1)) &&
44+
lazy
45+
) {
46+
// We only want to lazy inject/load the sdk bundle if
47+
// an error or promise rejection occured
48+
// OR someone called `capture...` on the SDK
49+
injectSdk(onLoadCallbacks);
50+
}
51+
queue.data.push(content);
52+
};
53+
queue.data = [];
54+
55+
function injectSdk(callbacks) {
56+
if (injected) {
57+
return;
58+
}
59+
injected = true;
60+
61+
// Create a `script` tag with provided SDK `url` and attach it just before the first, already existing `script` tag
62+
// Scripts that are dynamically created and added to the document are async by default,
63+
// they don't block rendering and execute as soon as they download, meaning they could
64+
// come out in the wrong order. Because of that we don't need async=1 as GA does.
65+
// it was probably(?) a legacy behavior that they left to not modify few years old snippet
66+
// https://www.html5rocks.com/en/tutorials/speed/script-loading/
67+
var _currentScriptTag = _document.scripts[0];
68+
var _newScriptTag = _document.createElement(_script);
69+
_newScriptTag.src = _sdkBundleUrl;
70+
_newScriptTag.crossOrigin = 'anonymous';
71+
72+
// Once our SDK is loaded
73+
_newScriptTag.addEventListener('load', function () {
74+
try {
75+
// Restore onerror/onunhandledrejection handlers - only if not mutated in the meanwhile
76+
if (_window[_onerror] && _window[_onerror].__SENTRY_LOADER__) {
77+
_window[_onerror] = _oldOnerror;
78+
}
79+
if (_window[_onunhandledrejection] && _window[_onunhandledrejection].__SENTRY_LOADER__) {
80+
_window[_onunhandledrejection] = _oldOnunhandledrejection;
81+
}
82+
83+
// Add loader as SDK source
84+
_window.SENTRY_SDK_SOURCE = 'loader';
85+
86+
var SDK = _window[_namespace];
87+
88+
var oldInit = SDK.init;
89+
90+
var integrations = [];
91+
if (_config.tracesSampleRate) {
92+
integrations.push(new Sentry.BrowserTracing());
93+
}
94+
95+
if (_config.replaysSessionSampleRate || _config.replaysOnErrorSampleRate) {
96+
integrations.push(new Sentry.Replay());
97+
}
98+
99+
if (integrations.length) {
100+
_config.integrations = integrations;
101+
}
102+
103+
// Configure it using provided DSN and config object
104+
SDK.init = function(options) {
105+
var target = _config;
106+
for (var key in options) {
107+
if (Object.prototype.hasOwnProperty.call(options, key)) {
108+
target[key] = options[key];
109+
}
110+
}
111+
oldInit(target);
112+
};
113+
114+
sdkLoaded(callbacks, SDK);
115+
} catch (o_O) {
116+
console.error(o_O);
117+
}
118+
});
119+
120+
_currentScriptTag.parentNode.insertBefore(_newScriptTag, _currentScriptTag);
121+
}
122+
123+
function sdkIsLoaded() {
124+
var __sentry = _window['__SENTRY__'];
125+
// If there is a global __SENTRY__ that means that in any of the callbacks init() was already invoked
126+
return !!(!(typeof __sentry === 'undefined') && __sentry.hub && __sentry.hub.getClient());
127+
}
128+
129+
function sdkLoaded(callbacks, SDK) {
130+
try {
131+
// We have to make sure to call all callbacks first
132+
for (var i = 0; i < callbacks.length; i++) {
133+
if (typeof callbacks[i] === 'function') {
134+
callbacks[i]();
135+
}
136+
}
137+
138+
var data = queue.data;
139+
140+
var initAlreadyCalled = sdkIsLoaded();
141+
142+
// Call init first, if provided
143+
data.sort((a, b) => a.f === 'init' ? -1 : 0);
144+
145+
// We want to replay all calls to Sentry and also make sure that `init` is called if it wasn't already
146+
// We replay all calls to `Sentry.*` now
147+
var calledSentry = false;
148+
for (var i = 0; i < data.length; i++) {
149+
if (data[i].f) {
150+
calledSentry = true;
151+
var call = data[i];
152+
if (initAlreadyCalled === false && call.f !== 'init') {
153+
// First call always has to be init, this is a conveniece for the user so call to init is optional
154+
SDK.init();
155+
}
156+
initAlreadyCalled = true;
157+
SDK[call.f].apply(SDK, call.a);
158+
}
159+
}
160+
if (initAlreadyCalled === false && calledSentry === false) {
161+
// Sentry has never been called but we need Sentry.init() so call it
162+
SDK.init();
163+
}
164+
165+
// Because we installed the SDK, at this point we have an access to TraceKit's handler,
166+
// which can take care of browser differences (eg. missing exception argument in onerror)
167+
var tracekitErrorHandler = _window[_onerror];
168+
var tracekitUnhandledRejectionHandler = _window[_onunhandledrejection];
169+
170+
// And now capture all previously caught exceptions
171+
for (var i = 0; i < data.length; i++) {
172+
if ('e' in data[i] && tracekitErrorHandler) {
173+
tracekitErrorHandler.apply(_window, data[i].e);
174+
} else if ('p' in data[i] && tracekitUnhandledRejectionHandler) {
175+
tracekitUnhandledRejectionHandler.apply(_window, [data[i].p]);
176+
}
177+
}
178+
} catch (o_O) {
179+
console.error(o_O);
180+
}
181+
}
182+
183+
// We make sure we do not overwrite window.Sentry since there could be already integrations in there
184+
_window[_namespace] = _window[_namespace] || {};
185+
186+
_window[_namespace].onLoad = function (callback) {
187+
onLoadCallbacks.push(callback);
188+
if (lazy && !forceLoad) {
189+
return;
190+
}
191+
injectSdk(onLoadCallbacks);
192+
};
193+
194+
_window[_namespace].forceLoad = function() {
195+
forceLoad = true;
196+
if (lazy) {
197+
setTimeout(function() {
198+
injectSdk(onLoadCallbacks);
199+
});
200+
}
201+
};
202+
203+
[
204+
'init',
205+
'addBreadcrumb',
206+
'captureMessage',
207+
'captureException',
208+
'captureEvent',
209+
'configureScope',
210+
'withScope',
211+
'showReportDialog'
212+
].forEach(function(f) {
213+
_window[_namespace][f] = function() {
214+
queue({ f: f, a: arguments });
215+
};
216+
});
217+
218+
// Store reference to the old `onerror` handler and override it with our own function
219+
// that will just push exceptions to the queue and call through old handler if we found one
220+
var _oldOnerror = _window[_onerror];
221+
_window[_onerror] = function() {
222+
// Use keys as "data type" to save some characters"
223+
queue({
224+
e: [].slice.call(arguments)
225+
});
226+
227+
if (_oldOnerror) _oldOnerror.apply(_window, arguments);
228+
};
229+
_window[_onerror].__SENTRY_LOADER__ = true;
230+
231+
// Do the same store/queue/call operations for `onunhandledrejection` event
232+
var _oldOnunhandledrejection = _window[_onunhandledrejection];
233+
_window[_onunhandledrejection] = function(e) {
234+
queue({
235+
p: 'reason' in e ? e.reason : 'detail' in e && 'reason' in e.detail ? e.detail.reason : e
236+
});
237+
if (_oldOnunhandledrejection) _oldOnunhandledrejection.apply(_window, arguments);
238+
};
239+
_window[_onunhandledrejection].__SENTRY_LOADER__ = true;
240+
241+
if (!lazy) {
242+
setTimeout(function () {
243+
injectSdk(onLoadCallbacks);
244+
});
245+
}
246+
})(
247+
window,
248+
document,
249+
'script',
250+
'onerror',
251+
'onunhandledrejection',
252+
'Sentry',
253+
'loader.js',
254+
__LOADER_BUNDLE__,
255+
__LOADER_OPTIONS__,
256+
__LOADER_LAZY__
257+
);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Sentry.captureException('Test exception');
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../../utils/fixtures';
4+
import { envelopeRequestParser,waitForErrorRequest } from '../../../../utils/helpers';
5+
6+
sentryTest('captureException works', async ({ getLocalTestUrl, page }) => {
7+
const req = waitForErrorRequest(page);
8+
9+
const url = await getLocalTestUrl({ testDir: __dirname });
10+
await page.goto(url);
11+
12+
const eventData = envelopeRequestParser(await req);
13+
14+
expect(eventData.message).toBe('Test exception');
15+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
window.doSomethingWrong();
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../../utils/fixtures';
4+
import { envelopeRequestParser,waitForErrorRequest } from '../../../../utils/helpers';
5+
6+
sentryTest('error handler works', async ({ getLocalTestUrl, page }) => {
7+
const req = waitForErrorRequest(page);
8+
9+
const url = await getLocalTestUrl({ testDir: __dirname });
10+
await page.goto(url);
11+
12+
const eventData = envelopeRequestParser(await req);
13+
expect(eventData.exception?.values?.length).toBe(1);
14+
expect(eventData.exception?.values?.[0]?.value).toBe('window.doSomethingWrong is not a function');
15+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
setTimeout(() => {
2+
window.doSomethingWrong();
3+
}, 500);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../../utils/fixtures';
4+
import { envelopeRequestParser,waitForErrorRequest } from '../../../../utils/helpers';
5+
6+
sentryTest('error handler works for later errors', async ({ getLocalTestUrl, page }) => {
7+
const req = waitForErrorRequest(page);
8+
9+
const url = await getLocalTestUrl({ testDir: __dirname });
10+
await page.goto(url);
11+
12+
const eventData = envelopeRequestParser(await req);
13+
14+
expect(eventData.exception?.values?.length).toBe(1);
15+
expect(eventData.exception?.values?.[0]?.value).toBe('window.doSomethingWrong is not a function');
16+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
window._testBaseTimestamp = performance.timeOrigin / 1000;

0 commit comments

Comments
 (0)