npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@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.

npm version TypeScript Bundle Size Gzipped Size

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-links

Usage

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 validation
  • options.openImmediately?: boolean | string - Whether to auto-open from URL (default: true), or a recording ID string to open immediately
  • options.parseJamData?: (input: string) => SerializableJamData | null - Custom URL parser
  • options.applyJamData?: (data: SerializableJamData) => string - Custom URL applier
  • options.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 type
  • listener: (event: CustomEvent) => void - Event handler
  • options?: 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 recording
    • recordingId: string - The recording ID to open
    • params.jamTitle?: string - Optional title for the recording
    • params.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 to session.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 to null to 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 parameters
  • session?: Session - Optional session (defaults to defaultSession from 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 to defaultSession from 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 from desktopCapturer.getSources()
  • callback: DisplayMediaRequestHandlerCallback - Callback from display media request handler
  • requester?: 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 desktopCapturer with 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=ID format URLs
  • Routes protocol requests to dual-window opener
  • Handles cross-platform differences (macOS open-url vs Windows/Linux second-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.