Creating the Crunchbase Chrome Extension Sidebar

Crunchbase is a prospecting platform that helps dealmakers find opportunities. Oftentimes, our users leverage Crunchbase in tandem with other platforms, searching for prospects on LinkedIn or browsing potential clients’ websites. 

Instead of switching between different tabs and tools, our users want to be able to access relevant Crunchbase data without leaving their current workflow. In early 2022 we launched the Crunchbase Chrome extension to bring contextual Crunchbase data into other sales tools and sites, including LinkedIn and Salesforce, with a custom Chrome sidebar.

Crunchbase Chrome extension on LinkedIn profile page
 

In this article, we’ll walk through the challenges we faced when building a Chrome extension sidebar and their solutions. We’ll use LinkedIn in the examples to make them more concrete.

Looking for a new opportunity?

The Crunchbase Engineering team is hiring! Check out our open roles.

How do you build a sidebar?

Various Chrome extensions add sidebars. Yet, there is no native API in Chrome to implement them, therefore, a custom solution is needed. We have two options:

  • Open a separate browser window, resize and position it next to the current one; or
  • Render a sidebar directly into the target web page’s DOM as an iframe.

While the first fully isolates the sidebar from the main page and vice versa (which is great), it provides a somewhat unusual user experience. It also might not play that well with window management tools. So we decided to use the second option and embed iframes directly into the web pages.

The content of our sidebar is sourced from crunchbase.com. Technically, it’s a part of our main web app. This way we are able to reuse a lot of existing UI elements and business logic from the main app.

Why is LinkedIn blocking our iframe?

We rendered our app as an iframe inside a LinkedIn page. As such, it’s subject to the Content Security Policy on that parent page.

CSP defined on the LinkedIn page determines what can be rendered in the iframe. LinkedIn restricts arbitrary third-party content from being embedded as iframes within its pages.

Interestingly enough, when the code that injects the iframe is a content script from our extension, it bypasses the CSP. The Chrome extension code is privileged in this regard. However, all subsequent page reloads inside the iframe are blocked by CSP.

Such page reloads can happen during development (with live reload). They can also happen during regular in-app navigation within LinkedIn — even though the parent LinkedIn page itself does not reload. This seems to be a bug in Chrome.

When it happens, the sidebar shows Chrome’s standard “Content is blocked” screen, and the error message in the console clarifies which policy prevented it from being loaded, for example:

Refused to frame 'https://www.crunchbase.com/' because it violates the following Content Security Policy directive: frame-src ...
 

To bypass CSP and prevent undesired blocking, we wrapped this iframe into yet another iframe sourced directly from the Chrome extension, not from the external crunchbase.com website. It looks like this:

wrapped iframe code block
 

The outer iframe, being sourced from the Chrome extension, is privileged and will not be blocked by the CSP even if it is forced to reload. And the inner iframe is no longer affected by LinkedIn’s CSP. Instead, it should honor the outer iframe’s CSP, which is up to us to define.

How do you handle cookies?

The second implication of serving the app in the iframe is handling cookies. Since the top-level page domain (www.linkedin.com) is different from the iframe source domain (www.crunchbase.com), the browser treats it as a third-party context.

Depending on the setup, third-party cookies can be blocked in a browser. When this happens, existing crunchbase.com cookies are omitted when the sidebar sends HTTP requests. The Set-Cookie headers in the responses are also ignored:

how to make cookies available chrome extension sidebar
 

Our regular session cookie becomes unavailable in the iframe, so it looks like a user is always logged out. For cookies to become available in the iframe we needed two things:

  • The browser to be configured to allow third-party cookies (it’s a default setting in Chrome, but a user can change it as they wish); and
  • The cookie to be created with the SameSite=None attribute, which also requires setting the Secure flag.

We cannot change the browser settings, but we can ask a user to do so. We detect the current browser setting by trying to write a cookie (also with the SameSite=None; Secure attributes) and read it right away. If we fail to read the same value we have just written, it means third-party cookies are blocked. In this case, we show a message to the user asking them to allow cookies to unlock all features of the extension.

We also needed to change the session cookie (and a few others, critical to the app) to be SameSite=None, instead of the default Lax. Naturally, this opens up the possibility of cross-site request forgery, so we needed to further mitigate that by implementing CSRF tokens. Angular has built-in support for that, so updating the front-end part to work with CSRF tokens was as simple as adding two lines of configuration.

How do you test a Chrome extension?

The Chrome extension’s functionality is provided by a combination of the extension itself and the app rendered in the iframe. The two pieces interact tightly with each other. We need to be able to test it end-to-end to make sure we don’t break things as we go.

First, we considered Cypress for testing, since that’s what we already use for other end-to-end tests at Crunchbase. We learned, however, it was not a good fit for testing a Chrome extension. Cypress renders the tested app in an iframe and keeps its own UI on the top-level page. The Chrome extension embeds its sidebar directly into the top-level page, outside of the Cypress app sandbox, so we would not be able to use Cypress assertions for the sidebar.

After some research, we went with Playwright. It nicely supports Chrome extensions and has recipes for some typical tasks: configuring a browser, detecting the extension id, and interacting with the service worker.

The only shortcoming we came across with Playwright is that it doesn’t expose any API to interact with the toolbar button added by the extension. This is more of a limitation of the Chrome browser rather than Playwright. A workaround for us was using the undocumented “dispatch” method on the event object in the context of the service worker:

async function clickBrowserAction(context: BrowserContext) {
  let [serviceWorker] = context.serviceWorkers();
  if (!serviceWorker)
    serviceWorker = await context.waitForEvent("serviceworker");

  serviceWorker.evaluate(() => {
    chrome.tabs.query({ active: true }, (tabs) => {
      (chrome.action.onClicked as any).dispatch(tabs[0]);
    });
  });
}
 

This emulates clicking on the button by programmatically dispatching a click event, without actually interacting with UI.

What’s next for the Crunchbase Chrome extension?

With the iframe approach above, we were able to quickly develop the Chrome extension and ship the much-requested functionality by reusing a lot of existing code from the Crunchbase codebase. And we managed to bypass restrictions imposed by the modern browser privacy model.

But we still generally depend on third-party cookies being enabled in the browser, which will not be available down the road. Chrome has announced it will stop supporting third-party cookies as a part of the Privacy Sandbox initiative.

In the next blog post, we’ll talk about how we’re addressing these upcoming changes, while still providing the full logged-in experience to our users. You can download the Crunchbase Chrome extension here.

Looking for a new opportunity?

The Crunchbase Engineering team is hiring! Check out our open roles.

  • Originally published January 5, 2023, updated January 6, 2023