- 1.6k words
- 8 mins
The Chrome inspector is just a webpage hosted locally inside the browser. If we could interact with its code using a console, we could access the API behind its interface.
The inspector’s own console can’t do that, since it’s just a fancy textbox that’s part of the webpage. It can run commands on the end-page it’s inspecting, but that’s about it.
That means all we need to do is inspect the inspector using another instance of itself!
I call this technique meta-inspection, and here is how it works:
- First, press
Ctrl + Shift + I
on the page you want to meta-inspect. - Detach the inspector into its own window.
- Press
Ctrl + Shift + I
again on the inspector window, opening up a second inspector. We’ll call it the meta-inspector! - Now you can reattach the first inspector, but don’t close it.
Now, just like with any other webpage, we can use the meta-inspector to do all kinds of things:
- Examine the inspector’s UI
- Put breakpoints in its code
- And if we get references to the right objects, call its internal API from the console!
That internal API has all the information about the end-page – the webpage we actually want to debug – and it’s all in the form of juicy JavaScript objects.
But why?
The power. Why else?
This technique lets you automate stuff that you could only do using the UI before. That opens up a world of possibilities:
- You can automate breakpoints
- Perform complex searches on live network data
- And do lots of other stuff!
Since it's an internal API, a lot of the code I’m going to show you might break in the future.
But the point of this post is the technique itself, not the specific code I’m going to use. The code is just an example of what’s possible!
I’ll show you exactly how I figured it out, so you can do the same if it breaks.
That said, I want to keep the examples working, so if they break do send me a line and I’ll fix them!
Inspector architecture
The inspector itself is written in TypeScript (surprise!), but you’ll only see the compiled JavaScript.
It might also be bundled and minified to some extent, which makes it a bit hard to work with.
You can view the source directly at the project repo, though, and I’m going to link to it quite often in this post.
Figuring stuff out
The code doesn’t have much documentation. It’s well-organized, but if you want to figure out how it works, your best bet is to start with the UI.
The inspector UI is divided into individual Panels, such as the
NetworkPanel
,
the
ConsolePanel
,
and so forth. Most display elements that can be dragged, toggled, or resized are
Panels of some sort.
Panels can contain other panels, as well as other components called Views.
With that in mind, let’s say you want to figure out how to do X using JavaScript. Your workflow is going to look something like this:
- Go to the source code of the Panel or View that’s related to X
- Figure out the API it uses to get data
- Follow that API to the correct lower-level component
So when I was trying to figure out how to get the network data, I first went to
the NetworkPanel
and quickly saw it uses the
NetworkLog
to get most of its information.
Whenever I got lost, I just went to the specific UI related to the thing I wanted.
For example, when I had trouble figuring out where to get the request payload, I
went to the
RequestPayloadView
.
Quick tip
You might notice that the meta-inspector doesn’t refresh, even if you refresh the end-page (the page we're inspecting) or navigate it somewhere else.
On one hand, it’s very convenient, since any functions and variables you defined earlier remain available.
But it also means the meta-inspector’s memory is going to fill up with data from lots of different pages, which can lead to unbounded memory usage.
You should be careful to refresh it every so often with a quick Ctrl + R
.
Importing stuff
The inspector uses ES modules, and we’ll need to dynamically import them in the meta-inspector’s console if we want to use its code.
While the modules themselves don’t change often, their import paths can change a lot, depending on the Chrome version and how it was built.
For example, the logs.js
module might be imported using one of the following
paths:
./models/logs/logs.js
./devtools-frontend/front_end/models/logs/logs.js
Luckily, there's a pretty stable API that lets us import modules using the same path. Here is how it works:
var Logs = await runtime.loadLegacyModule(
"models/logs/logs.js",
)
Internally, it just does a dynamic import from one of the inspector’s script files. Nothing fancy. But convenient!
Object architecture
The inspector API mostly consists of singleton classes. These classes mostly follow the same structure, which makes them easy to work with.
For example, we can access the
NetworkLog
instance from the Logs
module we imported earlier using:
var Logs = await runtime.loadLegacyModule(
"models/logs/logs.js",
)
var NetworkLog = Logs.NetworkLog.NetworkLog.instance()
Logs
is a module with an export NetworkLog
that also happens to be a module,
finally exporting the NetworkLog
class.
Here is a quick breakdown of the whole thing:
Processing network data
Let’s take a look at the first use case of this technique – filtering and processing network data using JavaScript.
We do this using the NetworkLog
I showed in the last section – it’s actually
the source of all the data in the Network panel. To access its data, we just
need to call:
NetworkLog.requests()
Which returns an array of
NetworkRequest
objects. These are mutable objects that get updated in real-time as network data
arrives.
These NetworkRequest
objects can also represent non-HTTP requests, blocked
requests, and things that aren’t really requests at all, like resolved data
URIs.
It can be helpful to exclude these using the methods
isHttpFamily
and
wasBlocked
NetworkLog.requests().filter((x) =>
x.isHttpFamily() &&
!x.wasBlocked()
)
Let’s take a look at some code examples!
Traffic volume by host
If you’re looking at a complicated web app with lots of dependencies, each making tons of different requests – you might want to know where most of the traffic is coming from.
Using the meta-inspector, you can figure it out using JavaScript. We just
group the log entries by
domain
and sum by
resourceSize
.
var trafficByHost = Object.create(null)
for (var rq of NetworkLog.requests()) {
let currentTraffic = trafficByHost[rq.domain ?? ""] ?? 0
currentTraffic += rq.resourceSize ?? 0
trafficByHost[rq.domain] = currentTraffic
}
trafficByHost
Security reports
TLS 1.2 is widely considered to be obsolete, but it’s still being used on the web in some cases.
We can use the NetworkLog
to summarize the security protocols used by each
request and find any that use TLS 1.2.
We do this using the
securityDetails()
method, which returns a raw
SecurityDetails
object.
This object gives lots of info about the security protocols, ciphers, key exchanges, and certificates used by each request.
Here is the code:
var reqList = NetworkLog.requests()
.map((rq) => {
return { // simplify objects:
url: rq.url(),
security: rq.securityDetails()?.protocol ?? "",
}
})
// Grouping requests by security protocol:
Object.groupBy(reqList, (x) => x.security)
I looked around, and I quickly found some webpages using TLS 1.2 using this technique:
Searching in request bodies
While we can search for stuff in response bodies using the inspector UI, that doesn’t work for searching inside request bodies. But using this technique, we can do it with code!
Getting the request payload is actually an async operation, so our code is going to be a bit more complicated than the earlier examples.
We’ll use the
requestFormData
method to retrieve the body of a request. It doesn’t just work for form-encoded
data. It just returns it as a string.
var searchPromises = NetworkLog.requests()
.filter((x) => x.requestMethod == "POST")
.map(async (x) => {
let payload = await x.requestFormData()
// skip no payload:
if (!payload) {
return false
}
// search in the contents:
if (!payload.includes("zionSp")) {
return false
}
// if pass, return simplified object:
return {
url: x.url(),
body: payload,
}
})// await all and filter out false values:
(await Promise.all(searchPromises)).filter((x) => x)
Other stuff
There's a lot more you can do with this network data. For example:
- Search for specific headers or header combinations.
- Analyze network timing across lots of requests.
- Search for all requests with a specific script in the initiator chain.
- Search for strings in responses from specific hosts
Conclusion
Accessing the inspector’s internal API is a bit complicated, but it’s a powerful debugging technique. If used correctly, it can literally save you hours of work.
In this post, I’ve mostly tackled how to use it for processing network data. In the future, I’ll tackle advanced DOM searches, automating breakpoints, and more!
What should I explore next? Let me know in the Discord!
Good luck, and have fun!