Skip to content

fix(loader): Ensure JS loader works with tracing & add tests #7662

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,60 @@ jobs:
cd packages/browser-integration-tests
yarn test:ci

job_browser_loader_tests:
name: Playwright Loader (${{ matrix.bundle }}) Tests
needs: [job_get_metadata, job_build]
if: needs.job_get_metadata.outputs.changed_browser_integration == 'true' || github.event_name != 'pull_request'
runs-on: ubuntu-20.04
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
bundle:
- loader_base
- loader_eager
- loader_tracing
- loader_replay
- loader_tracing_replay

steps:
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
uses: actions/checkout@v3
with:
ref: ${{ env.HEAD_COMMIT }}
- name: Set up Node
uses: volta-cli/action@v4
- name: Restore caches
uses: ./.github/actions/restore-cache
env:
DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }}
- name: Get npm cache directory
id: npm-cache-dir
run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
- name: Get Playwright version
id: playwright-version
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Check if Playwright browser is cached
id: playwright-cache
with:
path: ${{ steps.npm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-Playwright-${{steps.playwright-version.outputs.version}}
- name: Install Playwright browser if not cached
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps
env:
PLAYWRIGHT_BROWSERS_PATH: ${{steps.npm-cache-dir.outputs.dir}}
- name: Install OS dependencies of Playwright if cache hit
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: npx playwright install-deps
- name: Run Playwright Loader tests
env:
PW_BUNDLE: ${{ matrix.bundle }}
run: |
cd packages/browser-integration-tests
yarn test:loader

job_browser_integration_tests:
name: Browser (${{ matrix.browser }}) Tests
needs: [job_get_metadata, job_build]
Expand Down
8 changes: 7 additions & 1 deletion packages/browser-integration-tests/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ module.exports = {
node: true,
},
extends: ['../../.eslintrc.js'],
ignorePatterns: ['suites/**/subject.js', 'suites/**/dist/*', 'scripts/**'],
ignorePatterns: [
'suites/**/subject.js',
'suites/**/dist/*',
'loader-suites/**/dist/*',
'loader-suites/**/subject.js',
'scripts/**',
],
parserOptions: {
sourceType: 'module',
},
Expand Down
257 changes: 257 additions & 0 deletions packages/browser-integration-tests/fixtures/loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
/* eslint-disable */
// prettier-ignore
// Prettier disabled due to trailing comma not working in IE10/11
(function(
_window,
_document,
_script,
_onerror,
_onunhandledrejection,
_namespace,
_publicKey,
_sdkBundleUrl,
_config,
_lazy
) {
var lazy = _lazy;
var forceLoad = false;

for (var i = 0; i < document.scripts.length; i++) {
if (document.scripts[i].src.indexOf(_publicKey) > -1) {
// If lazy was set to true above, we need to check if the user has set data-lazy="no"
// to confirm that we should lazy load the CDN bundle
if (lazy && document.scripts[i].getAttribute('data-lazy') === 'no') {
lazy = false;
}
break;
}
}

var injected = false;
var onLoadCallbacks = [];

// Create a namespace and attach function that will store captured exception
// Because functions are also objects, we can attach the queue itself straight to it and save some bytes
var queue = function(content) {
// content.e = error
// content.p = promise rejection
// content.f = function call the Sentry
if (
('e' in content ||
'p' in content ||
(content.f && content.f.indexOf('capture') > -1) ||
(content.f && content.f.indexOf('showReportDialog') > -1)) &&
lazy
) {
// We only want to lazy inject/load the sdk bundle if
// an error or promise rejection occured
// OR someone called `capture...` on the SDK
injectSdk(onLoadCallbacks);
}
queue.data.push(content);
};
queue.data = [];

function injectSdk(callbacks) {
if (injected) {
return;
}
injected = true;

// Create a `script` tag with provided SDK `url` and attach it just before the first, already existing `script` tag
// Scripts that are dynamically created and added to the document are async by default,
// they don't block rendering and execute as soon as they download, meaning they could
// come out in the wrong order. Because of that we don't need async=1 as GA does.
// it was probably(?) a legacy behavior that they left to not modify few years old snippet
// https://www.html5rocks.com/en/tutorials/speed/script-loading/
var _currentScriptTag = _document.scripts[0];
var _newScriptTag = _document.createElement(_script);
_newScriptTag.src = _sdkBundleUrl;
_newScriptTag.crossOrigin = 'anonymous';

// Once our SDK is loaded
_newScriptTag.addEventListener('load', function () {
try {
// Restore onerror/onunhandledrejection handlers - only if not mutated in the meanwhile
if (_window[_onerror] && _window[_onerror].__SENTRY_LOADER__) {
_window[_onerror] = _oldOnerror;
}
if (_window[_onunhandledrejection] && _window[_onunhandledrejection].__SENTRY_LOADER__) {
_window[_onunhandledrejection] = _oldOnunhandledrejection;
}

// Add loader as SDK source
_window.SENTRY_SDK_SOURCE = 'loader';

var SDK = _window[_namespace];

var oldInit = SDK.init;

var integrations = [];
if (_config.tracesSampleRate) {
integrations.push(new Sentry.BrowserTracing());
}

if (_config.replaysSessionSampleRate || _config.replaysOnErrorSampleRate) {
integrations.push(new Sentry.Replay());
}

if (integrations.length) {
_config.integrations = integrations;
}

// Configure it using provided DSN and config object
SDK.init = function(options) {
var target = _config;
for (var key in options) {
if (Object.prototype.hasOwnProperty.call(options, key)) {
target[key] = options[key];
}
}
oldInit(target);
};

sdkLoaded(callbacks, SDK);
} catch (o_O) {
console.error(o_O);
}
});

_currentScriptTag.parentNode.insertBefore(_newScriptTag, _currentScriptTag);
}

function sdkIsLoaded() {
var __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());
}

function sdkLoaded(callbacks, SDK) {
try {
// We have to make sure to call all callbacks first
for (var i = 0; i < callbacks.length; i++) {
if (typeof callbacks[i] === 'function') {
callbacks[i]();
}
}

var data = queue.data;

var initAlreadyCalled = sdkIsLoaded();

// Call init first, if provided
data.sort((a, b) => a.f === 'init' ? -1 : 0);

// We want to replay all calls to Sentry and also make sure that `init` is called if it wasn't already
// We replay all calls to `Sentry.*` now
var calledSentry = false;
for (var i = 0; i < data.length; i++) {
if (data[i].f) {
calledSentry = true;
var call = data[i];
if (initAlreadyCalled === false && call.f !== 'init') {
// First call always has to be init, this is a conveniece for the user so call to init is optional
SDK.init();
}
initAlreadyCalled = true;
SDK[call.f].apply(SDK, call.a);
}
}
if (initAlreadyCalled === false && calledSentry === false) {
// Sentry has never been called but we need Sentry.init() so call it
SDK.init();
}

// Because we installed the SDK, at this point we have an access to TraceKit's handler,
// which can take care of browser differences (eg. missing exception argument in onerror)
var tracekitErrorHandler = _window[_onerror];
var tracekitUnhandledRejectionHandler = _window[_onunhandledrejection];

// And now capture all previously caught exceptions
for (var i = 0; i < data.length; i++) {
if ('e' in data[i] && tracekitErrorHandler) {
tracekitErrorHandler.apply(_window, data[i].e);
} else if ('p' in data[i] && tracekitUnhandledRejectionHandler) {
tracekitUnhandledRejectionHandler.apply(_window, [data[i].p]);
}
}
} catch (o_O) {
console.error(o_O);
}
}

// We make sure we do not overwrite window.Sentry since there could be already integrations in there
_window[_namespace] = _window[_namespace] || {};

_window[_namespace].onLoad = function (callback) {
onLoadCallbacks.push(callback);
if (lazy && !forceLoad) {
return;
}
injectSdk(onLoadCallbacks);
};

_window[_namespace].forceLoad = function() {
forceLoad = true;
if (lazy) {
setTimeout(function() {
injectSdk(onLoadCallbacks);
});
}
};

[
'init',
'addBreadcrumb',
'captureMessage',
'captureException',
'captureEvent',
'configureScope',
'withScope',
'showReportDialog'
].forEach(function(f) {
_window[_namespace][f] = function() {
queue({ f: f, a: arguments });
};
});

// Store reference to the old `onerror` handler and override it with our own function
// that will just push exceptions to the queue and call through old handler if we found one
var _oldOnerror = _window[_onerror];
_window[_onerror] = function() {
// Use keys as "data type" to save some characters"
queue({
e: [].slice.call(arguments)
});

if (_oldOnerror) _oldOnerror.apply(_window, arguments);
};
_window[_onerror].__SENTRY_LOADER__ = true;

// Do the same store/queue/call operations for `onunhandledrejection` event
var _oldOnunhandledrejection = _window[_onunhandledrejection];
_window[_onunhandledrejection] = function(e) {
queue({
p: 'reason' in e ? e.reason : 'detail' in e && 'reason' in e.detail ? e.detail.reason : e
});
if (_oldOnunhandledrejection) _oldOnunhandledrejection.apply(_window, arguments);
};
_window[_onunhandledrejection].__SENTRY_LOADER__ = true;

if (!lazy) {
setTimeout(function () {
injectSdk(onLoadCallbacks);
});
}
})(
window,
document,
'script',
'onerror',
'onunhandledrejection',
'Sentry',
'loader.js',
__LOADER_BUNDLE__,
__LOADER_OPTIONS__,
__LOADER_LAZY__
);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Sentry.captureException('Test exception');
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { expect } from '@playwright/test';

import { sentryTest } from '../../../../utils/fixtures';
import { envelopeRequestParser,waitForErrorRequest } from '../../../../utils/helpers';

sentryTest('captureException works', async ({ getLocalTestUrl, page }) => {
const req = waitForErrorRequest(page);

const url = await getLocalTestUrl({ testDir: __dirname });
await page.goto(url);

const eventData = envelopeRequestParser(await req);

expect(eventData.message).toBe('Test exception');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
window.doSomethingWrong();
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { expect } from '@playwright/test';

import { sentryTest } from '../../../../utils/fixtures';
import { envelopeRequestParser,waitForErrorRequest } from '../../../../utils/helpers';

sentryTest('error handler works', async ({ getLocalTestUrl, page }) => {
const req = waitForErrorRequest(page);

const url = await getLocalTestUrl({ testDir: __dirname });
await page.goto(url);

const eventData = envelopeRequestParser(await req);
expect(eventData.exception?.values?.length).toBe(1);
expect(eventData.exception?.values?.[0]?.value).toBe('window.doSomethingWrong is not a function');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
setTimeout(() => {
window.doSomethingWrong();
}, 500);
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { expect } from '@playwright/test';

import { sentryTest } from '../../../../utils/fixtures';
import { envelopeRequestParser,waitForErrorRequest } from '../../../../utils/helpers';

sentryTest('error handler works for later errors', async ({ getLocalTestUrl, page }) => {
const req = waitForErrorRequest(page);

const url = await getLocalTestUrl({ testDir: __dirname });
await page.goto(url);

const eventData = envelopeRequestParser(await req);

expect(eventData.exception?.values?.length).toBe(1);
expect(eventData.exception?.values?.[0]?.value).toBe('window.doSomethingWrong is not a function');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;
window._testBaseTimestamp = performance.timeOrigin / 1000;
Loading