ProxymanApp / Proxyman

Modern. Native. Delightful Web Debugging Proxy for macOS, iOS, and Android ⚡️
https://proxyman.io
5.48k stars 179 forks source link

Support GraphQL Previewer #412

Open NghiaTranUIT opened 4 years ago

NghiaTranUIT commented 4 years ago

🐶 Brief

Currently, it's super difficult to see the content of GraphQL requests even though in pretty JSON Previewer. The original discussion at https://www.reddit.com/r/macapps/comments/f7u7bb/proxyman_modern_and_delightful_web_debugging/fihjlwq?utm_source=share&utm_medium=web2x

Let see how we support it 🙌

👑 Criteria

atdrago commented 4 years ago

@NghiaTranUIT Loving Proxyman. I've already recommended it to a couple different people.

I'm curious about this issue. Would it allow Map Local and Map Remote for GraphQL queries and mutations? This is something I've wanted for a long time in Charles, and even sent in feature requests twice for.

In other words, I'd like to map requests to /graphql differently based on the request body. Map Local/Remote are based on path, but since all GraphQL requests typically go to the same path with a different body (operationName, variables, and query) it's impossible to use Map Local/Remote for GraphQL right now, as far as I know. Would that come along with this story or should I file a different issue?

Thank you!!

NghiaTranUIT commented 4 years ago

@atdrago Thank you for your kind words. Since I've just released the Proxyman v2 - which is rewritten with Swift- NIO (https://github.com/ProxymanApp/Proxyman/releases/tag/2.0.0), I totally free to push up new features.

GraphQL is what I'm looking for since there are not many tools that support debugging GraphQL.


You're right. At the moment, Map Local/Remote is based on the path, so it's impossible to map the content of the GraphQL. On the other hand, mapping base on operationName, variables, and query seems easier to implement.

I'm planning to support this soon 👍

atdrago commented 4 years ago

Thanks for your quick response!

This is so exciting to hear! I actually saw v2 come through. I updated and it inspired me to come check if anyone has requested GraphQL support yet. Keep up the great work!!!

nuynait commented 3 years ago

@atdrago I want to map local one of the graphQL queries.

One of my solution of map local graphQL queries are appending the query name into the URL. So domain.com/api/graphql changed to domain.com/api/graphql?gqlquery=MyQuery. I plan to modify the URL using the script tool.

Once the script tool has change the request url, I can then map local domain.com/api/graphql?gqlquery=MyQuery to change the response of only that query.

However, when I try this solution, I found that the map local is not applied. Here are some screenshots:

Captured 2020-09-22 at 15 36 34 Captured 2020-09-22 at 15 45 03 Captured 2020-09-22 at 15 46 28

An interesting fact, that if I choose repeat the request, the response is the local response I have in map local. When repeat the request, the URL is already with the gqlquery=MyQuery, that's why map local works.

It seems that map local only recognize the original url before it changed by the script tool.

It would be really cool if this works.

NghiaTranUIT commented 3 years ago

Hey @nuynait, at the moment, the Scripting will run after the Map Local. Therefore, it's a expected behavior.

To support GraphQL, I suggest that you can use

How to setup

function onRequest(context, url, request) {

// 1. Extract the queryName from the request var queryName = request.body.query.match(/\S+/gi)[1].split('(').shift();

// 2. Save to sharedState sharedState.queryName = queryName

// Done return request; }

function onResponse(context, url, request, response) {

// 3. Check if it's the request we need to map if (sharedState.queryName == "user") {

// 4. Import the local file by Action Button -> Import
// Get the local JSON file and set it as a body (like Map Local)
response.headers["Content-Type"] = "application/json";
response.body = file;

}

// Done return response; }



The above script will achieve the same result that you're trying 👍 
NghiaTranUIT commented 3 years ago

Meanwhile, it's correct that the Scripting Tool should be executed before other tools, such as Map Local, Remote, ...

I will revisit the logic and improve it 👍

nuynait commented 3 years ago

Hi @NghiaTranUIT Thanks for the quick and detailed reply. Thanks for all the tips on how to set up scripting so that I can map to the local response for GQL query. That's really helpful to someone who is new to Proxyman like me.

I would also be very interesting to know when you will be improving the logic since using Map Local tool instead of the scripting would be a much clean and interesting solution instead of doing the map in the scripting tool.

NghiaTranUIT commented 3 years ago

Ultimately, I will find a way to support GraphQL Rule, so we can define which query in the GraphQL body 👍

For now, I would suggest that using the Scripting tool 😄

TeresaCastle commented 3 years ago

Hi @NghiaTranUIT. I too am trying to figure out how to map local on graphql when there are multiple calls made, but only one endpoint. I used your script above, but am still not seeing my local file updating when I pull to refresh on the device when the script is running. If I repeat the request, an new request is displayed in the Proxyman and the response is updated with the file I want to replace it with from the script, but the changes do not display on the device. Could you help me figure out what I'm missing? Here is my script:

// /Users/teresa.castle/Library/Application Support/com.proxyman.NSProxy/users/AE397ECE.homepage_article.json const file = require("@users/AE397ECE.homepage_article.json"); // replace intercepted response with this file

function onRequest(context, url, request) { var operationName = request.body.query.match(/\S+/gi)[1].split('(').shift(); request.queries["gqlquery"]=operationName // adds operationName parameter sharedState.operationName = operationName console.log(sharedState.operationName) return request; }

function onResponse(context, url, request, response) { console.log(sharedState.operationName) if (sharedState.operationName == "appHomeScreen") { response.headers["Content-Type"] = "application/json"; response.body = file; // should replace with the file from the const file = require(....) from above }
return response; }

NghiaTranUIT commented 3 years ago

It seems correct @TeresaCastle, if you could not see the change, let add some console.log() to make sure it's correct.

For instance,

  1. I see you've already added console.log(sharedState.operationName) -> When making a request, do you see the log is appHomeScreen ?
  2. Do you see the log of console.log(sharedState.operationName) from onResponse() function?

It looks like the expression if (sharedState.operationName == "appHomeScreen") is false, so it the JSON file doesn't replace the Response Body

I suggest adding some console.log and debug to see if what is wrong 👍

TeresaCastle commented 3 years ago

I was seeing appHomeScreen on all of my console.log(). I think I see what is wrong. I have "appHomeScreen" and the console.log() is returning "AppHomeScreen". I've updated the script and it looks like it is working as expected.

I appreciate you taking the time to respond. I'm a manual tester with little scripting experience, but after reading your response, it took me 5 seconds to figure out what was wrong with what I had been staring at for 8 hours, lol.

Thanks so much!!!

NghiaTranUIT commented 3 years ago

Glad to know that it works for you 🎉

NghiaTranUIT commented 3 years ago

Just a friendly reminder: From build 2.20.0, we can quickly see the Query name of GraphQL requests on the main table.

It's super easier to distinguish graphQL request since the URLs are the same 🎉

CleanShot_2021-03-07_at_10_06_55_2x
atdrago commented 3 years ago

This is great and will definitely help identify the current GraphQL operation! Thank you @NghiaTranUIT !

Regarding my original comment wrt mapping local, we now add ?opname=${operationName} to the query string of our /graphql requests (as @nuynait and others have suggested). It solves the Map Local problem and makes Network logs way easier to read

NghiaTranUIT commented 3 years ago

Glad to know it helps you 😊

NghiaTranUIT commented 3 years ago

Hey guys, I'm working on this feature (#840), which allows us to define a rule by QueryName from GraphQL's Request Body. Therefore, you can use any debugging tools with GraphQL too 😄

NghiaTranUIT commented 3 years ago

Good news to @atdrago @TeresaCastle @nuynait @Elecweb @erickva and anyone who would like to use Breakpoint, Map Local with GraphQL request 🙌

Screen_Shot_2021-05-26_at_15_48_17 Screen_Shot_2021-05-26_at_15_46_13

If you guys find any problems/bugs, please report here. I appreciate it ❤️

NghiaTranUIT commented 3 years ago

Debugging with GraphQL feature is available on the latest build (2.27.0). Please update it 😄

Thanks all 🙇

erickva commented 3 years ago

@NghiaTranUIT This is incredible news. I am no longer working on a Project with GraphQL, but it will happen again soon. Great effort! It will be a life saver. ❤️

cameroncooke commented 3 years ago

Does this work with mutations as well or just queries?

NghiaTranUIT commented 3 years ago

It works with mutation too @cameroncooke

For instance, the QueryName is ViewerNotificationsLastSeenUpdate in this graphQL Body.

{
"query": "mutation ViewerNotificationsLastSeenUpdate{response:viewerNotificationsLastSeenUpdate(input:{}){node{id notificationFeedItemsUnreadCount notificationFeedLastSeenAt __typename}__typename}}"
}