-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix(browser): Ensure lazyLoadIntegration
works in NPM mode
#11673
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import * as Sentry from '@sentry/browser'; | ||
|
||
// So we can use this in subject.js | ||
// We specifically DO NOT set this on window.Sentry as we want to test a non-CDN bundle environment, | ||
// where window.Sentry is usually not available | ||
window._testSentry = { ...Sentry }; | ||
|
||
Sentry.init({ | ||
dsn: 'https://[email protected]/1337', | ||
integrations: [], | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
window._testLazyLoadIntegration = async function run() { | ||
const integration = await window._testSentry.lazyLoadIntegration('httpClientIntegration'); | ||
|
||
window._testSentry.getClient()?.addIntegration(integration()); | ||
|
||
window._integrationLoaded = true; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { expect } from '@playwright/test'; | ||
import { SDK_VERSION } from '@sentry/browser'; | ||
|
||
import { sentryTest } from '../../../../utils/fixtures'; | ||
|
||
sentryTest('it allows to lazy load an integration when using the NPM package', async ({ getLocalTestUrl, page }) => { | ||
const bundle = process.env.PW_BUNDLE || ''; | ||
// We only want to run this in non-CDN bundle mode | ||
if (bundle.startsWith('bundle')) { | ||
sentryTest.skip(); | ||
} | ||
|
||
const url = await getLocalTestUrl({ testDir: __dirname }); | ||
|
||
await page.route(`https://browser.sentry-cdn.com/${SDK_VERSION}/httpclient.min.js`, route => { | ||
return route.fulfill({ | ||
status: 200, | ||
contentType: 'application/javascript;', | ||
body: "window.Sentry = window.Sentry || {};window.Sentry.httpClientIntegration = () => ({ name: 'HttpClient' })", | ||
}); | ||
}); | ||
|
||
await page.goto(url); | ||
|
||
const hasIntegration = await page.evaluate('!!window._testSentry.getClient().getIntegrationByName("HttpClient")'); | ||
expect(hasIntegration).toBe(false); | ||
|
||
const scriptTagsBefore = await page.evaluate<number>('document.querySelectorAll("script").length'); | ||
|
||
await page.evaluate('window._testLazyLoadIntegration()'); | ||
await page.waitForFunction('window._integrationLoaded'); | ||
|
||
const scriptTagsAfter = await page.evaluate<number>('document.querySelectorAll("script").length'); | ||
|
||
const hasIntegration2 = await page.evaluate('!!window._testSentry.getClient().getIntegrationByName("HttpClient")'); | ||
expect(hasIntegration2).toBe(true); | ||
|
||
expect(scriptTagsAfter).toBe(scriptTagsBefore + 1); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,12 +33,15 @@ const WindowWithMaybeIntegration = WINDOW as { | |
export async function lazyLoadIntegration(name: keyof typeof LazyLoadableIntegrations): Promise<IntegrationFn> { | ||
const bundle = LazyLoadableIntegrations[name]; | ||
|
||
if (!bundle || !WindowWithMaybeIntegration.Sentry) { | ||
// `window.Sentry` is only set when using a CDN bundle, but this method can also be used via the NPM package | ||
const sentryOnWindow = (WindowWithMaybeIntegration.Sentry = WindowWithMaybeIntegration.Sentry || {}); | ||
|
||
if (!bundle) { | ||
throw new Error(`Cannot lazy load integration: ${name}`); | ||
} | ||
|
||
// Bail if the integration already exists | ||
const existing = WindowWithMaybeIntegration.Sentry[name]; | ||
const existing = sentryOnWindow[name]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. related to my comment above: Assuming we already installed the integration in NPM mode during the normal Again, I feel like I lack some context so feel free to disregard! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this case specifically I thought about too, my idea was that we can/should check for this outside of this method - my reasoning was that otherwise we'd have to also pass the integration name (e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc @ryan953 also relates to your comment. So to clarify we have to options IMHO:
function maybeLoadFeedback() {
if(client.getIntegrationByName('Feedback')) {
return;
}
return lazyLoadIntegration('feedbackIntegration');
}
2.a: For me, the main reason I went with 1. was that if a user uses this, they probably don't need this check (think in the future we want users to use this when using the loader to add pluggable integrations), and thus we can keep the API surface & bundle size slightly smaller. But I'm also totally fine with adding this additional check. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like 1. for what it's worth. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok yes, that makes sense, let's go with 1 then. Thx for explaining! |
||
if (typeof existing === 'function') { | ||
return existing; | ||
} | ||
|
@@ -61,7 +64,7 @@ export async function lazyLoadIntegration(name: keyof typeof LazyLoadableIntegra | |
throw new Error(`Error when loading integration: ${name}`); | ||
} | ||
|
||
const integrationFn = WindowWithMaybeIntegration.Sentry[name]; | ||
const integrationFn = sentryOnWindow[name]; | ||
|
||
if (typeof integrationFn !== 'function') { | ||
throw new Error(`Could not load integration: ${name}`); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I'm missing some context here but since we never set
window.Sentry
in NPM mode, should we really start doing it now? Just wondering since we have the__SENTRY__
carrier object anyway 🤔There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is that the integration CDN bundles always add to
window.Sentry
, so we can't really influence this here (unless we change the logic in our CDN bundles, which is also possible of course 🤔 )Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I see the dilemma 😅
I wouldn't do that - CDN users definitely expect
window.Sentry
APIs and probably also integrations being available this way.So I guess the lesser of the two evils is to also create a
window.Sentry
object in NPM mode. I still have the feeling that this might bite us into the back but we can worry about that if it comes up. I guess the one concrete concern would be users who create awindow.Sentry
object themselves. I think I've seen this before in some unsupported SSR framework but I didn't find the issue/discussion that I had in mind. And even in this case, we only add a property to the object and wouldn't overwrite it if it was there previously. So 🤞There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, but even then we'd put the integration onto that object, which is not ideal but should generally also not break stuff (hopefully xD)