Recording Links: The Nitty Gritty Details Behind Today's Launch
Behind the scenes of how we built the first magic link for bug reports!

Earlier today we announced Recording Links, the easiest way for Product teams to capture screen recordings, repro steps, and user feedback directly inside their app, without requiring a Chrome extension or install.
To do so, we had to:
- Design a simple + attractive recorder UX you’d happily send to your users
- Make sure it works reliably and in every major browser vendor
- Keep the install process as simple as it can be, and no simpler
As one of the authors on this project I’m clearly biased, but I think the solution we’ve shipped is just shy of magic. And although magicians never reveal their secrets, I write software and have been asked to cook for a minute about our work. So I’ll just tell you: we did it all with <iframe>
s.

But before we go too deep: try Recording Links yourself, or check out this demo Ian recorded!
Prior Art
Recording Links builds on Jam’s core extension and more-recent Jam for Customer Support (Intercom) products. Both of these are built on Jam’s core object model and capture stack, which allow us to deliver a consistent debugging experience in our own app and the others we integrate with.

While each of our products have somewhat different feature sets, Jam’s customers expect a few core promises from our products:
- Users can easily start, stop, restart, and submit recordings
- Events must be captured between start of recording and end of recording
- We can’t lose data along the way, lest our users (or worse, yours!) lose trust in us
Fulfilling these promises in the extension can sometimes be a challenge. But, since users’ data is local, it’s at least reasonably straightforward to fulfill our promises.
This was comparatively difficult in our Intercom product. Specifically, that product sends users to a Recorder hosted at recorder.jam.dev
, which—due to browser security constraints—uses a websocket to communicate with event capture scripts installed on our users’ sites.
The websocket approach works fine enough, but has room for improvement:
- If our socket server is unavailable—e.g. briefly during deploys, or a user is recording while offline—created Jams may be corrupted or missing data; there is no way for the recorder script to recognize this state and inform the user.
- Common Internet chaos—e.g. slow connections, out-of-order packets, unexpected user and/or script interactions—requires mitigation, thereby increasing code complexity and ongoing maintenance
- It literally doesn’t work in Safari—and may someday not work in other browsers—because it relies on third-party cookies to establish a shared identifier
One of our design goals for Recording Links was to remove this network dependency altogether. The only time we expect to hear from your users is when they’re explicitly submitting a Jam.
State Partitioning
This lofty design goal introduces a classic problem: “how do we communicate between tabs locally?”. Most browser primitives (e.g. localStorage, BroadcastChannels…) are restricted to same-origin communication. Only Safari is so strict (Chrome and Firefox restrict to same-domain), but since same-origin is our lowest-common denominator let’s stick with it.
Our earlier architecture—a Jam-hosted recorder.jam.dev
URL—can’t communicate directly with scripts on pages you host, because it’s a different origin. But what if we put an <iframe>
on your pages from the same-origin as our recorder
? Well, that’s not so simple. Here are @kentcdodds and @ryanflorence discussing the issue in Oct 2023:

All browsers use a technique called “State Partitioning” to prevent cross-site tracking. What this means practically is that recorder.jam.dev frames embedded on example.com belong to a “state partition” keyed by the top-frame’s origin (i.e. example.com:recorder.jam.dev), whereas our top frame is in the top-level partition (i.e. recorder.jam.dev).

recorder.jam.dev
cannot communicate with the embedded oneWe started by exploring how to bring your origin to our contents, e.g. by asking installers to set up a recorder.example.com subdomain and point it to our content. But since Safari is strictly same-origin, we cannot assume that recorder.example.com can natively communicate with example.com. In fact, doing so would fail our expectation of supporting all major browser vendors.

recorder.example.com
is same-domain but not same-origin to example.com
Our next idea was to bring our contents to your origin, by embedding both the Capture and the Recorder scripts onto your pages via iframe. This way, both capture.js and recorder.js scripts would live in the example.com:recorder.jam.dev partition, and could communicate to each other!

recorder.jam.dev
are in the same state partitionThe rest of the work was not easy—try building an app that works equally well in embedded vs. non-embedded states, or reliably streaming video content across an iframe boundary in all major desktop browsers—but it was mostly app work, on top of infrastructure we could trust.
Managed Embeds
Having determined our architecture requirements, we also prioritized figuring out exactly how we would distribute our iframes. We had previously considered a few other mechanisms—the CNAME approach of course, and a “proxy recorder” where embedders’ servers fetched and served our content directly—but there were only two meaningful mechanisms in our iframe bucket:
- DIRECT EMBED—where embedders directly embed an iframe with a URL we provide:
<html>
<head>
<title>Recorder Page</title>
<!-- YOUR PAGE'S `<head>` -->
</head>
<body>
<iframe src="https://recorder.jam.dev/recorder/PASS-THE-ID"
sandbox="allow-scripts allow-same-origin allow-popups">
</iframe>
</body>
</html>
- MANAGED EMBED—where embedders use a script we provide to mount our iframe:
<html>
<head>
<!-- on all capture-able pages -->
<script type="module" src="<https://js.jam.dev/capture.js>"></script>
<!-- at least on recorder pages -->
<script type="module" src="<https://js.jam.dev/recorder.js>"></script>
</head>
</html>
We also wanted to make sure that Jams recorded on your pages only get sent to you—you don’t want someone from evil.com
to spoof an example.com
Recording Link and steal the privileged network and console logs from an unsuspecting user.
To achieve this, we needed to design a domain verification strategy. We considered three options:
- Over DNS—configure a TEXT record with a value we expect

- Over HTTP—return a response we expect from a configured endpoint
const express = require('express');
const app = express();
app.get("/PROVIDED-URL", (_req, res) => res.send("PROVIDED-BODY"));
- Over HTML—attest the teams you allow wherever you load the recorder script
<html>
<head>
<script type="text/javascript" src="https://js.jam.dev/recorder.js"></script>
<meta name="jam:team" content="TEAM-ID-COPIED-FROM-SETTINGS" />
<meta name="jam:team" content="ANOTHER-TEAM-ID-FROM-SETTINGS" />
<!-- more head content -->
</head>
<body>
<!-- body content -->
</body>
</html>
Consider the above options with regards to three criteria we’d set out while spec’ing the work:
- Prefer copy/paste-able solutions vs. ones that require config
- Prefer solutions implementable by lower-authority personnel
- Prefer solutions that look familiar and obvious vs. strange and opaque
Managed Embeds w/ In-HTTP Domain Verification is just… cleaner. There’s only one line that requires config, and we don’t have to ask users to update their site code should we need different iframe attributes, for example. Additionally, neither of these require the implementer have permission to edit DNS settings. And everyone recognizes script embeds and meta tags!
As with above, the rest of the work was not easy—coordinating state across multiple frame boundaries is challenging!—but we thought it much more convenient for users and maintainable for us to own this complexity. I’m still impressed we managed to pack such a powerful product into three lines of HTML!
What's Next?
We have ambitious plans for Recording Links: new ways to create them, new ways to consume them, new ways to make them even easier to use. Today, we celebrate how far we’ve come; Recording Links has surpassed many of our expectations internally, and we hope it delivers you not just the value but the wows we’ve been experiencing.
Have you tried Recording Links yet? If not, what are you waiting for? It’s only 3 lines of HTML to get started; I’d order you a coffee while you work, but let’s be real—if you made it this far, by the time the coffee was ready you’d already be done.