Skip to content

Commit bdc04f4

Browse files
committed
test(loader): Add integration tests for JS loader behavior
1 parent 120ae6d commit bdc04f4

File tree

33 files changed

+769
-52
lines changed

33 files changed

+769
-52
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_performance
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: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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+
// This is not supported in tests, so we disable it here
71+
_newScriptTag.crossOrigin = 'anonymous';
72+
73+
// Once our SDK is loaded
74+
_newScriptTag.addEventListener('load', function () {
75+
try {
76+
// Restore onerror/onunhandledrejection handlers
77+
_window[_onerror] = _oldOnerror;
78+
_window[_onunhandledrejection] = _oldOnunhandledrejection;
79+
80+
// Add loader as SDK source
81+
_window.SENTRY_SDK_SOURCE = 'loader';
82+
83+
var SDK = _window[_namespace];
84+
85+
var oldInit = SDK.init;
86+
87+
var integrations = [];
88+
if (_config.tracesSampleRate) {
89+
integrations.push(new Sentry.BrowserTracing());
90+
}
91+
92+
if (_config.replaysSessionSampleRate || _config.replaysOnErrorSampleRate) {
93+
integrations.push(new Sentry.Replay());
94+
}
95+
96+
if (integrations.length) {
97+
_config.integrations = integrations;
98+
}
99+
100+
// Configure it using provided DSN and config object
101+
SDK.init = function(options) {
102+
var target = _config;
103+
for (var key in options) {
104+
if (Object.prototype.hasOwnProperty.call(options, key)) {
105+
target[key] = options[key];
106+
}
107+
}
108+
oldInit(target);
109+
};
110+
111+
sdkLoaded(callbacks, SDK);
112+
113+
// Ensure we load the SDK in non-lazy mode, even if no Sentry.onLoad() was provided
114+
// If it were provided, it would have already been called by sdkLoaded() above.
115+
if (!sdkIsLoaded() && !lazy) {
116+
SDK.init();
117+
}
118+
} catch (o_O) {
119+
console.error(o_O);
120+
}
121+
});
122+
123+
_currentScriptTag.parentNode.insertBefore(_newScriptTag, _currentScriptTag);
124+
}
125+
126+
function sdkIsLoaded() {
127+
var __sentry = _window['__SENTRY__'];
128+
// If there is a global __SENTRY__ that means that in any of the callbacks init() was already invoked
129+
return !!(!(typeof __sentry === 'undefined') && __sentry.hub && __sentry.hub.getClient());
130+
}
131+
132+
function sdkLoaded(callbacks, SDK) {
133+
try {
134+
// We have to make sure to call all callbacks first
135+
for (var i = 0; i < callbacks.length; i++) {
136+
if (typeof callbacks[i] === 'function') {
137+
callbacks[i]();
138+
}
139+
}
140+
141+
var data = queue.data;
142+
143+
var initAlreadyCalled = sdkIsLoaded();
144+
145+
// Call init first, if provided
146+
data.sort((a, b) => a.f === 'init' ? -1 : 1);
147+
148+
console.log(JSON.stringify(data, null, 2));
149+
150+
// We want to replay all calls to Sentry and also make sure that `init` is called if it wasn't already
151+
// We replay all calls to `Sentry.*` now
152+
var calledSentry = false;
153+
for (var i = 0; i < data.length; i++) {
154+
if (data[i].f) {
155+
calledSentry = true;
156+
var call = data[i];
157+
if (initAlreadyCalled === false && call.f !== 'init') {
158+
// First call always has to be init, this is a conveniece for the user so call to init is optional
159+
SDK.init();
160+
}
161+
initAlreadyCalled = true;
162+
SDK[call.f].apply(SDK, call.a);
163+
}
164+
}
165+
if (initAlreadyCalled === false && calledSentry === false) {
166+
// Sentry has never been called but we need Sentry.init() so call it
167+
SDK.init();
168+
}
169+
170+
// Because we installed the SDK, at this point we have an access to TraceKit's handler,
171+
// which can take care of browser differences (eg. missing exception argument in onerror)
172+
var tracekitErrorHandler = _window[_onerror];
173+
var tracekitUnhandledRejectionHandler = _window[_onunhandledrejection];
174+
175+
// And now capture all previously caught exceptions
176+
for (var i = 0; i < data.length; i++) {
177+
if ('e' in data[i] && tracekitErrorHandler) {
178+
tracekitErrorHandler.apply(_window, data[i].e);
179+
} else if ('p' in data[i] && tracekitUnhandledRejectionHandler) {
180+
tracekitUnhandledRejectionHandler.apply(_window, [data[i].p]);
181+
}
182+
}
183+
} catch (o_O) {
184+
console.error(o_O);
185+
}
186+
}
187+
188+
// We make sure we do not overwrite window.Sentry since there could be already integrations in there
189+
_window[_namespace] = _window[_namespace] || {};
190+
191+
_window[_namespace].onLoad = function (callback) {
192+
onLoadCallbacks.push(callback);
193+
if (lazy && !forceLoad) {
194+
return;
195+
}
196+
injectSdk(onLoadCallbacks);
197+
};
198+
199+
_window[_namespace].forceLoad = function() {
200+
forceLoad = true;
201+
if (lazy) {
202+
setTimeout(function() {
203+
injectSdk(onLoadCallbacks);
204+
});
205+
}
206+
};
207+
208+
209+
[
210+
'init',
211+
'addBreadcrumb',
212+
'captureMessage',
213+
'captureException',
214+
'captureEvent',
215+
'configureScope',
216+
'withScope',
217+
'showReportDialog'
218+
].forEach(function(f) {
219+
_window[_namespace][f] = function() {
220+
queue({ f: f, a: arguments });
221+
};
222+
});
223+
224+
// Store reference to the old `onerror` handler and override it with our own function
225+
// that will just push exceptions to the queue and call through old handler if we found one
226+
var _oldOnerror = _window[_onerror];
227+
_window[_onerror] = function (message, source, lineno, colno, exception) {
228+
// Use keys as "data type" to save some characters"
229+
queue({
230+
e: [].slice.call(arguments)
231+
});
232+
233+
if (_oldOnerror) _oldOnerror.apply(_window, arguments);
234+
};
235+
_window[_onerror].__SENTRY_LOADER__ = true;
236+
_window.__SENTRY_ORIGINAL_ONERROR__ = _oldOnerror;
237+
238+
239+
// Do the same store/queue/call operations for `onunhandledrejection` event
240+
var _oldOnunhandledrejection = _window[_onunhandledrejection];
241+
_window[_onunhandledrejection] = function(e) {
242+
queue({
243+
p: 'reason' in e ? e.reason : 'detail' in e && 'reason' in e.detail ? e.detail.reason : e
244+
});
245+
if (_oldOnunhandledrejection) _oldOnunhandledrejection.apply(_window, arguments);
246+
};
247+
248+
if (!lazy) {
249+
setTimeout(function () {
250+
injectSdk(onLoadCallbacks);
251+
});
252+
}
253+
})(
254+
window,
255+
document,
256+
'script',
257+
'onerror',
258+
'onunhandledrejection',
259+
'Sentry',
260+
'loader.js',
261+
__LOADER_BUNDLE__,
262+
__LOADER_OPTIONS__,
263+
__LOADER_LAZY__
264+
);
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+
});

0 commit comments

Comments
 (0)