Skip to content
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

Implement Puppeteer adapter for Polly that works in web workers in order to test extensions #13870

Closed
wants to merge 27 commits into from

Conversation

marekweb
Copy link
Contributor

@marekweb marekweb commented Sep 16, 2020

Add a new PuppeteerAdapter for Polly for intercepting HTTP requests.

Current status: this is a draft PR and includes a temporary test script (run withts-node test-puppeteer-adapter.ts). There are still some errors that happen while intercepting some types of requests.

Problem background

This solves the limitation that the default Puppeteer adapter for Polly does not work to intercept requests inside of web workers.

This limitation is blocking us from writing integration tests for Sourcegraph extensions.

Issues: #12194 #12180
PR which is blocked: #12195

Implementation

The new PuppeteerAdapter uses puppeteer-interceptor which is a wrapper around the Chrome DevTools Protocol.

It uses CDP's "Fetch domain" to intercept requests, which seems to be the best way to do it right now -- however this part of CDP is still flagged as experimental. Actually, I don't see any mention in the docs of it being experimental, so that may have been outdated information.

Alternatives to consider

Because puppeteer-interceptor makes it a bit difficult the follow the flow of the code, and because it's a relatively small wrapper around CDP calls, we could use CDP directly instead and it might result in a cleaner solution. This could be a next step after we get this current solution working properly.

Update and resolution

The above is no longer current. I abandoned the puppeteer-interceptor approach and implemented the alternative instead. See the update comments on this PR for the details.

@marekweb
Copy link
Contributor Author

marekweb commented Sep 23, 2020

After some trial and error on this PR, I created CdpAdapter: an adapter that calls the Chrome DevTools Protocol directly (the Fetch domain of CDP) instead of doing it through a library.

The advantages of this new adapter over the earlier PuppeteerAdapter:

  • it removes the dependency on puppeteer-interceptor
  • simplified control flow: no control promises involved
  • it removes a layer of indirection between Polly and CDP, and makes more straightforward to convert between their respective request/response formats

What's left on CdpAdapter:

  • Implement the request pattern matching options. Currently it captures all requests. Decided to exclude this from the scope of the current PR
  • Validate if request recording works correctly New issue created: Implement Polly recording/replaying of requests #14174
  • Ignore errors from sending CDP requests after the page was closed.
  • Handle failed requests properly, which means proper conversion between Polly and CDP failed requests.

Copy link
Contributor

@felixfbecker felixfbecker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipped the console.logs and TODOs, this all looks really good!

@felixfbecker felixfbecker added ops & tools & dev team/web testing Issues that deal with unit tests, integration tests and the testing infrastructure. labels Sep 23, 2020
@marekweb marekweb marked this pull request as ready for review September 25, 2020 04:09
@marekweb
Copy link
Contributor Author

marekweb commented Sep 25, 2020

The Polly adapter types definition (the Adapter base class that we use) is missing some types (namely the constructor argument and the entire handleRequest method).

I tried to fix the types by adding declarations, or inlining a fixed declaration, but I didn't find a working solution. I spent too much time on the problem already so I kept two @ts-ignore comments.

* interception using the CDP "Fetch domain".
*/
public async onConnect(): Promise<void> {
console.log('CDP adapter connecting')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
console.log('CDP adapter connecting')

@@ -96,7 +96,7 @@ export const createSharedIntegrationTestContext = async <
directory,
}: IntegrationTestOptions): Promise<IntegrationTestContext<TGraphQlOperations, TGraphQlOperationNames>> => {
await driver.newPage()
await driver.page.setRequestInterception(true)
// await driver.page.setRequestInterception(true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// await driver.page.setRequestInterception(true)

@@ -0,0 +1,71 @@
import { Polly } from '@pollyjs/core'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this file be deleted?

@felixfbecker
Copy link
Contributor

Closing this in favor of #12195, since @marekweb merged this branch into it

* return a Promise of the Response for that request, which will be passed to
* request.respond().
*/
public passthroughRequest(pollyRequest: PollyRequest & PollyRequestArguments): Promise<PollyResponse> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name of this method is a bit misleading. Its purpose is to create a brand new request given the pollyRequest argument and return the response. This is used internally by both the record and passthrough handlers to get the response data for the given pollyRequest. The reason a duplicate request is necessary is because you don't want to respond to the original request until polly is done with it (i.e. via mutations with client side server events) and also the actual request could have been modified during the request event which means you might be making a completely different request than the original.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would the current implementation of this be an acceptable simplification if we don't make use of request mutation anywhere (we don't)? Or is there a reason that wouldn't work? Iirc the duplicate requests posed some problems because they can't be 1:1 identical in some regard but I don't fully remember

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would the current implementation of this be an acceptable simplification if we don't make use of request mutation anywhere

I would say most likely not since this is how all adapters are currently expected to behave. I mostly see usages of the request event when wanting to change auth headers or user credentials so I feel like this is pretty important to try incorporate. We could list this as a 3rd party adapter and document this as a caveat until we can get this working properly.

Iirc the duplicate requests posed some problems because they can't be 1:1 identical in some regard but I don't fully remember

The biggest reason we ran into issues was because we were making native browser fetch requests which were an issue when the intercepted request was made via XHR. I don't think you'll run into the same issues here but I'm sure there might be some other complexities as I'm not fully familiar with CDP.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I see, I agree that it should be solved before we upstream this. Do you think there would be a problem with using this in our test suite this way though given that we know that we don't change auth headers or credentials etc?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, i think it should be fine as long as you don't actually fully respond to the original request until respondToRequest is called.

public async onConnect(): Promise<void> {
this.cdpSession = await this.page.target().createCDPSession()

// TODO: This is where we narrow down the interception with patterns.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also add a check to see if you're already intercepting requests on the page.

Fetch example:

this.assert(
      'Running concurrent fetch adapters is unsupported, stop any running Polly instances.',
      !context.fetch[IS_STUBBED] && !context.Request[IS_STUBBED]
    );

requestId,
responseCode: pollyResponse.statusCode,
responseHeaders: headerObjectToHeaderEntries(headers),
body: toBase64(body),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the body always have to be in base64? That will be unexpected for users that are expecting JSON or text. Can we detect if the body is utf8 via import { isBufferUtf8Representable } from '@pollyjs/utils'?

@keegancsmith keegancsmith deleted the lg/puppeteer-cdp branch January 27, 2023 20:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ops & tools & dev testing Issues that deal with unit tests, integration tests and the testing infrastructure.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants