@jam.dev/recording-links
v0.3.1
Published
Capture bug reports from your users with the Jam recording links SDK
Readme
@jam.dev/recording-links
A TypeScript SDK for loading & initializing Jam Recording Links scripts across browser tabs and windows.
Overview
This SDK provides a lightweight interface for coordinating Jam recording sessions across multiple browser contexts. It handles dynamic script loading, cross-tab state synchronization, and recording lifecycle management.
Benefits
- ⚡ Dynamic script loading: uses dynamic imports for just-in-time script loading
- 🚀 Auto-initialization: uses URL state and cross-tab reference counting to auto-load when necessary
- 🌐 Cross-browser support: supports all modern ES2020+ browsers
- 🔧 Bundler compatibility: pre-declares dynamic imports as external for popular bundlers
- 📡 Event system: subscribe to SDK events like script loading
- 📦 TypeScript support: full type definitions included
Installation
npm install @jam.dev/recording-linksUsage
Basic Setup
import * as jam from "@jam.dev/recording-links/sdk";
// Initialize the SDK
jam.initialize();
// Load the recorder when needed
const Recorder = await jam.loadRecorder();
Recorder.open("recording-id");Auto-loading from URL
The SDK automatically detects jam-recording and jam-title URL parameters and loads the recorder:
// On a page with URL: https://example.com?jam-recording=abc123&jam-title=My%20Recording
jam.initialize(); // Automatically loads recorder and opens recording "abc123"Custom Configuration
jam.initialize({
teamId: "team-123,team-456", // Manually provide team ID(s); default: reads `<meta name="jam:team">` from the page
openImmediately: false, // Don't auto-open recorder from URL params
// OR: "recording-123" to auto-open a recording by ID
parseJamData: (href) => {
// Custom logic to extract recording data from URL
const url = new URL(href);
return {
recordingId: url.searchParams.get("jam-recording"),
jamTitle: url.searchParams.get("jam-title")
};
},
applyJamData: (data) => {
// Custom logic to apply recording data to URL
const url = new URL(window.location.href);
url.searchParams.set("jam-recording", data.recordingId);
url.searchParams.set("jam-title", data.jamTitle);
return url.href;
}
});Event Listeners
// Listen for SDK events
jam.addEventListener("loaded", (event) => {
console.log(`Script loaded: ${event.detail.name}`); // "capture" or "recorder"
});API Reference
initialize(options?)
Initializes the SDK with optional configuration.
Parameters:
options.teamId?: string- Team ID for recording validationoptions.openImmediately?: boolean | string- Whether to auto-open from URL (default:true), or a recording ID string to open immediatelyoptions.parseJamData?: (input: string) => SerializableJamData | null- Custom URL parseroptions.applyJamData?: (data: SerializableJamData) => string- Custom URL applieroptions.recorderRefCounter?: RefCounter- Custom reference counter (advanced)
Throws:
- Error if SDK is already initialized
loadRecorder(options?)
Loads the Jam recorder module and returns a promise that resolves to the recorder interface.
Parameters:
Omit<InitializeOptions, "recorderRefCounter">
Returns:
Promise<RecorderSingleton>- The Recorder singleton
Throws:
- Error if SDK is not initialized
isInitialized()
Returns whether the SDK has been initialized.
Returns:
boolean- True if the SDK has been initialized, false otherwise
addEventListener(type, listener, options?)
Add an event listener for SDK events.
Parameters:
type: "loaded"- Event typelistener: (event: CustomEvent) => void- Event handleroptions?: AddEventListenerOptions- Standard event listener options
removeEventListener(type, listener, options?)
Remove an event listener for SDK events.
Recorder
The loaded recorder singleton (null until loadRecorder() is called).
Methods:
open(recordingId, params?)- Open recorder for specific recordingrecordingId: string- The recording ID to openparams.jamTitle?: string- Optional title for the recordingparams.removeOnEscape?: boolean- Whether to close recorder on Escape (default: true)
Types
The SDK exports several TypeScript types:
ScriptName
type ScriptName = "capture" | "recorder";SerializableJamData
type SerializableJamData = {
recordingId: string | null;
jamTitle: string | null;
};InitializeOptions
type InitializeOptions = {
teamId?: string;
openImmediately?: boolean | string | undefined | null;
parseJamData?(input: string): SerializableJamData | null;
applyJamData?(data: SerializableJamData): string;
recorderRefCounter?: RefCounter;
};RecorderSingleton
type RecorderSingleton = {
open(recordingId: string, params?: {
jamTitle?: string | null;
removeOnEscape?: boolean;
}): unknown;
};Electron
Jam Recording Links can be integrated into your Electron app to enable seamless bug reporting with screen recordings.
1. Initialize Jam Electron (Required)
Initialize the SDK in your main process before app.whenReady():
import { app, BrowserWindow } from "electron";
import * as jam from "@jam.dev/recording-links/electron";
jam.initialize({
// Optional: Customize the session if using non-default or multiple sessions.
// A Recorder can only capture logs from windows belonging to the same session.
// Defaults to session.defaultSession if not provided.
// defaultSession: session.defaultSession,
// Optional: Customize the display media request handler.
// By default, Jam installs a handler that auto-selects capture sources.
// Set to null if you want to handle display media requests yourself.
// defaultDisplayMediaRequestHandler: null,
openRecorderWindow(session) {
return new BrowserWindow({
width: 1000,
height: 700,
webPreferences: {
// Required to allow loading Jam's remote recorder scripts from *.jam.dev.
// Alternative: Use session.webRequest.onHeadersReceived() to set CSP headers
// allowing *.jam.dev for default-src, script-src, and worker-src.
allowRunningInsecureContent: true,
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
},
});
},
async loadRecorderPage(win, data) {
// Load your app with jam-* query parameters
return win.loadURL(`https://my-app.com${data.search}`);
// or: return win.loadFile("index.html", data);
},
});What this does:
- Installs display media request handler for screen capture on the default session
- Configures window creation and content loading for recorder
- Manages recorder window lifecycle per session
2. Handle Incoming Jam Links (Recommended)
Register your app's protocol (if not already done):
Add to your package.json electron-builder config:
{
"build": {
"protocols": {
"name": "my-app",
"schemes": ["myapp"]
}
}
}Wire up protocol handlers to open Jam recordings:
import { app } from "electron";
import * as jam from "@jam.dev/recording-links/electron";
// macOS: Handle protocol URLs
app.on("open-url", (event, url) => {
event.preventDefault();
const [cleanUrl, recorderWindow] = jam.openUrl(url);
if (recorderWindow) {
recorderWindow.focus();
}
// cleanUrl has jam-* params removed for your main window
});
// Windows/Linux: Handle second instance
// Note: Remember to implement app.requestSingleInstanceLock() in your app
app.on("second-instance", (event, commandLine) => {
const url = commandLine.find((arg) => arg.startsWith("myapp://"));
if (url) {
const [cleanUrl, recorderWindow] = jam.openUrl(url);
if (recorderWindow) {
recorderWindow.focus();
}
}
});Result: Links like myapp://open?jam-recording=abc123 will launch your app and open the recorder.
3. Enable In-App Recording (Optional)
Add a menu item to trigger recordings from within your app:
import { Menu } from "electron";
import * as jam from "@jam.dev/recording-links/electron";
const template = [
{
label: "Help",
submenu: [
{
label: "Report a Bug",
click: () => {
jam.openRecorder("your-recording-id-here");
},
},
],
},
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));Test: Click "Report a Bug" → Recorder window opens and screen capture starts.
API Reference
initialize(config)
Initializes the Jam SDK for Electron. Must be called before using other Jam functions.
Parameters:
config.defaultSession?: Session- The session to install display media handler on. Defaults tosession.defaultSession. If using multiple sessions, install handlers on each session.config.defaultDisplayMediaRequestHandler?: DisplayMediaRequestHandler | null- The display media request handler to install. In an upcoming version, this will introduce a "screen or window picker" UI to the Recorder window. Right now, it uses the OS picker on Mac, and default-selects the first window it sees in Windows + Linux. Set tonullto handle display media requests yourself (see advanced example below).config.openRecorderWindow(session: Session): BrowserWindow(required) - Function that creates and returns a BrowserWindow for the recorder. Called once per session when opening a recorder.config.loadRecorderPage(win: BrowserWindow, data: IJamData): Promise<void>(required) - Function that loads your app with Jam recording parameters into the recorder window.
Advanced Example with Custom Display Media Handler:
import { BrowserWindow, desktopCapturer, session, webContents } from "electron";
import * as jam from "@jam.dev/recording-links/electron";
jam.initialize({
defaultDisplayMediaRequestHandler: null, // Don't install default handler
// ... other config
});
// Install custom handler per session
session.defaultSession.setDisplayMediaRequestHandler(
async (request, callback) => {
const frame = request.frame;
const contents = frame ? webContents.fromFrame(frame) : null;
const win = contents ? BrowserWindow.fromWebContents(contents) : null;
if (jam.isJamRecorder(win)) {
const sources = await desktopCapturer.getSources({
types: ["screen", "window"],
});
jam.handleDisplayMediaRequest(sources, callback, win);
} else {
// Your app's own display media handling
callback({});
}
},
{ useSystemPicker: true },
);openRecorder(init, session?)
Opens a Jam recorder window. If a recorder window is already open for the session, focuses it and loads the new recording.
Parameters:
init: string | { recordingId: string; jamTitle?: string } | URLSearchParams- Recording ID, data object, or URL parameterssession?: Session- Optional session (defaults todefaultSessionfrom initialize)
Returns: BrowserWindow - The recorder window (returns immediately, page loads asynchronously)
Examples:
// Open by recording ID
jam.openRecorder("abc123");
// Open with title
jam.openRecorder({ recordingId: "abc123", jamTitle: "Bug Report" });
// Open from URL parameters
const params = new URLSearchParams("jam-recording=abc123&jam-title=Bug");
jam.openRecorder(params);openUrl(url, session?)
Parses a URL, extracts Jam recording parameters, and opens the recorder if parameters are found. Returns the cleaned URL (with Jam params removed) and the recorder window if opened.
Parameters:
url: string | URL- URL to parse (e.g., from protocol handler)session?: Session- Optional session (defaults todefaultSessionfrom initialize)
Returns: [string, BrowserWindow | null] - Tuple of cleaned URL and recorder window (or null)
Example:
app.on("open-url", (event, url) => {
event.preventDefault();
const [cleanUrl, recorderWindow] = jam.openUrl(url);
if (recorderWindow) {
recorderWindow.focus();
} else {
// Open your main window with cleanUrl
}
});isJamRecorder(win)
Checks if a BrowserWindow is a Jam recorder window.
Parameters:
win: BrowserWindow | null- Window to check
Returns: boolean - True if window is a Jam recorder
Example:
import { BrowserWindow } from "electron";
import * as jam from "@jam.dev/recording-links/electron";
const win = BrowserWindow.getFocusedWindow();
if (jam.isJamRecorder(win)) {
console.log("This is a Jam recorder window");
}handleDisplayMediaRequest(sources, callback, requester?)
Helper function for custom display media request handlers. Filters sources (excludes recorder window and DevTools), selects the best source, and calls the callback.
In an upcoming version, this function will implement a handshake with the Recorder window to display a "screen or window picker" UI. Right now, it auto-selects the first app window (or first screen if no windows are available).
Parameters:
sources: DesktopCapturerSource[]- Sources fromdesktopCapturer.getSources()callback: DisplayMediaRequestHandlerCallback- Callback from display media request handlerrequester?: BrowserWindow | null- The requesting window (to exclude from sources)
Example:
import { BrowserWindow, desktopCapturer, session, webContents } from "electron";
import * as jam from "@jam.dev/recording-links/electron";
session.defaultSession.setDisplayMediaRequestHandler(
async (request, callback) => {
const sources = await desktopCapturer.getSources({
types: ["screen", "window"],
});
const frame = request.frame;
const win = frame
? BrowserWindow.fromWebContents(webContents.fromFrame(frame))
: null;
jam.handleDisplayMediaRequest(sources, callback, win);
},
);IJamData Interface
Data structure representing Jam recording parameters.
interface IJamData {
recordingId: string; // Jam recording ID
jamTitle: string | undefined | null; // Optional recording title
state: string | undefined | null; // Optional JWT state
searchParams: URLSearchParams; // jam-* query parameters
search: string; // Query string with '?' prefix (or empty)
}What Jam Recording Links Enables for Your Electron App
Users can report bugs with one click:
- From your app menu → Instant screen recording starts
- From Jam Recording Links (via Slack, email) → App launches and recording begins
- Zero configuration of screen capture or window management
What the SDK Provides
Window Architecture
- Creates separate recorder window when
jam-*query parameters detected - Maintains main window alongside recorder (multi-window pattern)
- Auto-focuses or reuses existing recorder window if already open
- Prevents recorder window from closing while recording
Screen Capture
- Filters capture sources to exclude recorder window (prevents recursive capture)
- Auto-excludes DevTools windows from source list
- Configures
desktopCapturerwith appropriate permissions - TEMPORARY: Implements source selection strategy: prefer app windows, then screens
- WILL BE REPLACED with user source picker in future release
Protocol Integration
- Parses
yourapp://open?jam-recording=IDformat URLs - Routes protocol requests to dual-window opener
- Handles cross-platform differences (macOS
open-urlvs Windows/Linuxsecond-instance) - Validates and extracts
jam-*query parameters
Developer Configuration
You provide:
- Protocol scheme: Your app's custom URL scheme (e.g., "notion", "slack")
- Web URLs: Dev server (e.g.,
http://localhost:3000) and production path - Recording IDs: Which Jam recording to open for bug reports (coming soon: dynamic values via API)
- Trigger UI: Where to place menu items or buttons in your app
SDK handles everything else.
Contributing
See CONTRIBUTING.md for development setup, build commands, and release process.
