@jam.dev/recording-links 0.3.0-electron.5 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@ A TypeScript SDK for loading & initializing Jam Recording Links scripts across b
9
9
 
10
10
  ## Overview
11
11
 
12
- 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.
12
+ This SDK provides a lightweight, zero-dependency interface for coordinating Jam recording sessions across multiple browser contexts. It handles dynamic script loading, cross-tab state synchronization, and recording lifecycle management.
13
13
 
14
14
  ## Benefits
15
15
 
@@ -58,19 +58,34 @@ jam.initialize({
58
58
  openImmediately: false, // Don't auto-open recorder from URL params
59
59
  // OR: "recording-123" to auto-open a recording by ID
60
60
  parseJamData: (href) => {
61
- // Custom logic to extract recording data from URL
61
+ // Custom logic to extract recording data from URL.
62
+ // By default, Jam looks for `jam-` prefixed query parameters.
63
+ // If your app rewrites Recording Links into a different format,
64
+ // you might do something like this:
62
65
  const url = new URL(href);
63
- return {
64
- recordingId: url.searchParams.get("jam-recording"),
65
- jamTitle: url.searchParams.get("jam-title")
66
- };
66
+ const jamParams = {} as { [K in `jam-${string}`]: string | null; };
67
+
68
+ for (const [key, value] of url.searchParams.entries<>()) {
69
+ if (key.startsWith("my-custom-prefix-")) {
70
+ jamParams[key.replace(/^my-custom-prefix-/, "jam-") as `jam-${string}`] = value;
71
+ }
72
+ }
73
+
74
+ return jamParams;
67
75
  },
68
- applyJamData: (data) => {
69
- // Custom logic to apply recording data to URL
76
+ applyJamData: (data: { [K in `jam-${string}`]: string | null }) => {
77
+ // Custom logic to apply recording data to URL.
78
+ // Similar to the above, if your app uses a non-standard format,
79
+ // you can rewrite the parameters here:
70
80
  const url = new URL(window.location.href);
71
81
 
72
- url.searchParams.set("jam-recording", data.recordingId);
73
- url.searchParams.set("jam-title", data.jamTitle);
82
+ for (const [key, value] of Object.entries(data)) {
83
+ if (value) {
84
+ url.searchParams.set(key.replace(/^jam-/, "my-custom-prefix-"), value);
85
+ } else {
86
+ url.searchParams.delete(key);
87
+ }
88
+ }
74
89
 
75
90
  return url.href;
76
91
  }
package/lib/electron.d.ts CHANGED
@@ -145,7 +145,7 @@ export declare function initialize(config: {
145
145
  * jam.openRecorder('abc123');
146
146
  *
147
147
  * // Open with title
148
- * jam.openRecorder({ recordingId: 'abc123', title: 'Bug Report' });
148
+ * jam.openRecorder({ recordingId: 'abc123', jamTitle: 'Bug Report' });
149
149
  *
150
150
  * // Open with URLSearchParams (from protocol handler)
151
151
  * const params = new URLSearchParams('jam-recording=abc123&jam-title=Bug+Report');
@@ -162,10 +162,6 @@ export declare function openUrl(url: string | URL, ses?: Session): [string, Brow
162
162
  * Data structure representing Jam recording parameters.
163
163
  */
164
164
  export interface IJamData {
165
- /** Jam recording ID */
166
- readonly recordingId: string;
167
- /** Optional recording title */
168
- readonly title: string | undefined | null;
169
165
  /**
170
166
  * URLSearchParams containing jam-* query parameters.
171
167
  * Follows the same convention as URL.searchParams.
@@ -178,13 +174,11 @@ export interface IJamData {
178
174
  readonly search: string;
179
175
  }
180
176
  declare class JamData implements IJamData {
181
- readonly recordingId: string;
182
- readonly title: string | undefined | null;
177
+ private params;
183
178
  get searchParams(): URLSearchParams;
184
179
  get search(): string;
185
180
  constructor(init: URLSearchParams | {
186
- recordingId: string;
187
- title?: string | null;
181
+ [K in `jam-${string}`]: string;
188
182
  });
189
183
  }
190
184
  export declare function isJamRecorder(win: BrowserWindow | null): boolean;
package/lib/electron.js CHANGED
@@ -1 +1 @@
1
- import{app as e,session as r,webContents as t,BrowserWindow as n,desktopCapturer as o}from"electron";const s={defaultSession:null,windows:new Map,openRecorderWindow(){throw new Error("Not initialized")},loadRecorderPage(){throw new Error("Not initialized")}},i=(e,r)=>{const s=e.frame,i=s?t.fromFrame(s):null,a=i?n.fromWebContents(i):null;u(a)?o.getSources({types:["screen","window"]}).then(e=>f(e,r,a)).catch(e=>{r({})}):r({})};async function a(t){if(!e.isReady())return e.whenReady().then(()=>a(t));const{defaultSession:n=r.defaultSession,defaultDisplayMediaRequestHandler:o=i}=t;s.defaultSession=n,s.openRecorderWindow=t.openRecorderWindow,s.loadRecorderPage=t.loadRecorderPage,o&&n.setDisplayMediaRequestHandler(o,{useSystemPicker:!0})}function d(e,r){const t=r??s.defaultSession;if(null===t)throw new Error("Cannot open recorder: no `session` found or provided");let n=s.windows.get(t);n||(n=s.openRecorderWindow(t),s.windows.set(t,n),n.on("closed",()=>s.windows.delete(t)));const o="string"==typeof e?new l({recordingId:e}):e instanceof l?e:new l(e);return s.loadRecorderPage(n,o).catch(e=>{}),n.isMinimized()&&n.restore(),n.focus(),n}function c(e,r){const t="string"==typeof e?new URL(e):e,n=new URLSearchParams;for(const[e,r]of t.searchParams.entries())e.startsWith("jam-")&&(n.set(e,r),t.searchParams.delete(e));return[t.href,n.has("jam-recording")?d(new l(n),r):null]}class l{get searchParams(){const e=new URLSearchParams;return e.set("jam-recording",this.recordingId),this.title&&e.set("jam-title",this.title),e}get search(){const e=this.searchParams.toString();return e?`?${e}`:""}constructor(e){if(e instanceof URLSearchParams){const r=e.get("jam-recording");if(!r)throw new Error("Missing jam-recording parameter");this.recordingId=r,this.title=e.get("jam-title")}else this.recordingId=e.recordingId,this.title=e.title||null}}function u(e){for(const r of s.windows.values())if(r===e)return!0;return!1}function f(e,r,t){const n=t?.getMediaSourceId(),o=e.filter(e=>!e.name.includes("DevTools")&&e.id!==n),s=o.find(e=>e.id.startsWith("window:")),i=o.find(e=>e.id.startsWith("screen:")),a=s||i||o[0];r(a?{video:a,audio:"loopback"}:{})}export{f as handleDisplayMediaRequest,a as initialize,u as isJamRecorder,d as openRecorder,c as openUrl};//# sourceMappingURL=electron.js.map
1
+ import{app as e,session as r,webContents as n,BrowserWindow as o,desktopCapturer as t}from"electron";const s={defaultSession:null,windows:new Map,openRecorderWindow(){throw new Error("Not initialized")},loadRecorderPage(){throw new Error("Not initialized")}},a=(e,r)=>{const s=e.frame,a=s?n.fromFrame(s):null,i=a?o.fromWebContents(a):null;w(i)?t.getSources({types:["screen","window"]}).then(e=>u(e,r,i)).catch(e=>{r({})}):r({})};async function i(n){if(!e.isReady())return e.whenReady().then(()=>i(n));const{defaultSession:o=r.defaultSession,defaultDisplayMediaRequestHandler:t=a}=n;s.defaultSession=o,s.openRecorderWindow=n.openRecorderWindow,s.loadRecorderPage=n.loadRecorderPage,t&&o.setDisplayMediaRequestHandler(t,{useSystemPicker:!0})}function d(e,r){const n=r??s.defaultSession;if(null===n)throw new Error("Cannot open recorder: no `session` found or provided");let o=s.windows.get(n);o||(o=s.openRecorderWindow(n),s.windows.set(n,o),o.on("closed",()=>s.windows.delete(n)));const t="string"==typeof e?new l({"jam-recording":e}):e instanceof l?e:new l(e);return s.loadRecorderPage(o,t).catch(e=>{}),o.isMinimized()&&o.restore(),o.focus(),o}function c(e,r){const n="string"==typeof e?new URL(e):e,o=new URLSearchParams;for(const[e,r]of n.searchParams.entries())e.startsWith("jam-")&&(o.set(e,r),n.searchParams.delete(e));return[n.href,o.has("jam-recording")?d(new l(o),r):null]}class l{get searchParams(){return new URLSearchParams(this.params)}get search(){const e=this.params.toString();return e?`?${e}`:""}constructor(e){const r=new URLSearchParams(e);if(!r.get("jam-recording"))throw new Error("Missing jam-recording parameter");this.params=r}}function w(e){for(const r of s.windows.values())if(r===e)return!0;return!1}function u(e,r,n){const o=n?.getMediaSourceId(),t=e.filter(e=>!e.name.includes("DevTools")&&e.id!==o),s=t.find(e=>e.id.startsWith("window:")),a=t.find(e=>e.id.startsWith("screen:")),i=s||a||t[0];r(i?{video:i,audio:"loopback"}:{})}export{u as handleDisplayMediaRequest,i as initialize,w as isJamRecorder,d as openRecorder,c as openUrl};//# sourceMappingURL=electron.js.map
package/lib/sdk.d.ts CHANGED
@@ -22,15 +22,18 @@ type RecorderSingleton = {
22
22
  * Opens a recorder for the specified recording ID.
23
23
  * @param recordingId - The ID of the recording to open
24
24
  * @param params - Optional parameters for opening the recorder
25
- * @param params.jamTitle - Optional title for the recording
26
25
  * @param params.removeOnEscape - Whether to remove the opened recorder on Escape presses. Default: true
27
26
  * @param params.applyJamData - Custom function to apply recording data to URL
27
+ * @param params.jamParams - Querystring parameters to set when opening the recorder
28
+ * e.g. `jam-title` to set the recording title, `jam-state` to set JWT state
28
29
  * @returns Unknown - TODO: expose a public API for opened recorders
29
30
  */
30
31
  open(recordingId: string, params?: Pick<InitializeOptions, "applyJamData"> & {
31
- jamTitle?: string | null;
32
32
  /** Whether to remove the opened recorder on Escape presses. Default: true */
33
33
  removeOnEscape?: boolean;
34
+ jamParams?: {
35
+ [key: string]: string | null;
36
+ };
34
37
  }): unknown;
35
38
  };
36
39
  type InitializeOptions = {
@@ -54,12 +57,12 @@ type InitializeOptions = {
54
57
  */
55
58
  openImmediately?: boolean | string | undefined | null;
56
59
  /**
57
- * Extract a `recordingId` and `jamTitle` from the provided string.
58
- * Defaults to reading the `jam-recording` and `jam-title` QSPs off the URL.
60
+ * Extract a `jam-recording`, `jam-title`, and other `jam-` params from the provided string.
61
+ * Defaults to reading the `jam-recording`, `jam-title`, and `jam-state` QSPs off the URL.
59
62
  */
60
63
  parseJamData?(input: string): SerializableJamData | null;
61
64
  /**
62
- * Applies a `recordingId` and `jamTitle` when the JamRecorder is opened or closes.
65
+ * Applies a `jam-recording`, `jam-title`, and other `jam-` params when the JamRecorder is opened or closes.
63
66
  * Defaults to setting (or rm'ing) the `jam-recording` and `jam-title` QSPs.
64
67
  * Setting a custom `parseJamData` disables this default.
65
68
  */
@@ -67,14 +70,27 @@ type InitializeOptions = {
67
70
  /** Selectors to blur on host pages while the Jam Recorder is recording. */
68
71
  blurSelectors?: string | string[] | (() => string | string[]);
69
72
  };
73
+ /**
74
+ * Internal options accepted by {@link initialize}. Extends the public
75
+ * {@link InitializeOptions} with `_loadRemoteScript`, a private seam the SDK's own
76
+ * tests use to inject a fake remote-script loader instead of importing from the CDN.
77
+ *
78
+ * `_loadRemoteScript` is deliberately kept off the public {@link InitializeOptions} —
79
+ * consumers must never pass it. It must, however, live on the type that annotates
80
+ * `initialize`'s parameter: destructuring it off the public type instead leaks into the
81
+ * emitted declaration as a property `InitializeOptions` doesn't have, surfacing
82
+ * `TS2339: Property '_loadRemoteScript' does not exist on type 'InitializeOptions'` in
83
+ * every consumer build without `skipLibCheck` (CORE-2500).
84
+ */
85
+ type InitializeInternalOptions = InitializeOptions & {
86
+ /** @internal Test/override seam — loads `recorder`/`capture` from the Jam CDN. */
87
+ _loadRemoteScript?: (name: ScriptName) => Promise<any>;
88
+ };
70
89
  /**
71
90
  * Data structure representing Jam recording metadata that can be serialized to/from URLs.
72
91
  */
73
92
  export type SerializableJamData = {
74
- /** The unique identifier of the recording, or null if not present */
75
- recordingId: string | null;
76
- /** The human-readable title of the recording, or null if not present */
77
- jamTitle: string | null;
93
+ [K in `jam-${string}`]: string | null;
78
94
  };
79
95
  declare function resetForTesting(): void;
80
96
  /** @internal - Reset SDK state for testing (only available in dev builds) */
@@ -128,7 +144,7 @@ export declare let Recorder: RecorderSingleton | null;
128
144
  * });
129
145
  * ```
130
146
  */
131
- export declare function initialize({ recorderRefCounter, _loadRemoteScript, ...config }?: InitializeOptions): void;
147
+ export declare function initialize({ recorderRefCounter, _loadRemoteScript, ...config }?: InitializeInternalOptions): void;
132
148
  /**
133
149
  * Loads the Jam recorder module and returns the recorder singleton.
134
150
  *
@@ -159,7 +175,7 @@ export declare function initialize({ recorderRefCounter, _loadRemoteScript, ...c
159
175
  * });
160
176
  *
161
177
  * // Use the recorder
162
- * recorder.open("recording-xyz789", { jamTitle: "Bug Report" });
178
+ * recorder.open("recording-xyz789", { jamParams: { "jam-title": "Bug Report" } });
163
179
  * ```
164
180
  */
165
181
  export declare function loadRecorder({ ...config }?: Omit<InitializeOptions, "recorderRefCounter">): Promise<RecorderSingleton>;
package/lib/sdk.js CHANGED
@@ -1,4 +1,4 @@
1
- function e(){const e=new EventTarget;return{addEventListener(t,n,r){e.addEventListener(t,n,r)},removeEventListener(t,n,r){e.removeEventListener(t,n,r)},dispatch:(t,n)=>e.dispatchEvent(new CustomEvent(t,{detail:n}))}}const t=(t,n=localStorage)=>{const r=`jam:${t}`,i=e();let o=null;try{o=n.getItem(r)}catch(e){}const a={count:Number.parseInt(o??"0",10)||0,addEventListener:i.addEventListener.bind(i),removeEventListener:i.removeEventListener.bind(i),update(e){const t=a.count;switch(e){case"increment":a.count+=1;break;case"decrement":a.count-=1;break;default:a.count=e}if(a.count<0&&(a.count=0),a.count!==t){try{n.setItem(r,`${a.count}`)}catch(e){}Object.assign(a,{count:a.count}),i.dispatch("update",a.count)}return a.count}};return window.addEventListener("storage",e=>{if(e.storageArea===n&&e.key===r){const t=Number.parseInt(e.newValue??"",10);Number.isNaN(t)||t===a.count||(a.count=t,i.dispatch("update",a.count))}}),a},n=e=>{try{const t=new URL(e);return{recordingId:t.searchParams.get("jam-recording"),jamTitle:t.searchParams.get("jam-title")}}catch(e){}return null},r={isInitialized:!1},i=void 0,o=e(),a=o.addEventListener.bind(o),c=o.removeEventListener.bind(o),d=()=>r.isInitialized;let s=null;function u({recorderRefCounter:e=t("numRecorders"),_loadRemoteScript:i=m,...o}={}){if(r.isInitialized)throw new Error("SDK already initialized.");Object.assign(r,{isInitialized:!0,recorderRefCounter:e,config:o,_loadRemoteScript:i}),e.count>0?p():e.addEventListener("update",p,{once:!0}),window.addEventListener("popstate",c);const a={apply(e,t,n){const r=Reflect.apply(e,t,n);return c(),r}};function c(){if(s)return;const e=r.config?.parseJamData??n,t="string"==typeof r.config?.openImmediately?r.config.openImmediately:e(window.location.href)?.recordingId;t&&l({openImmediately:!1!==r.config?.openImmediately&&t})}history.pushState=new Proxy(history.pushState,a),history.replaceState=new Proxy(history.replaceState,a),c()}async function l({...e}={}){if(s)return s;if(!r.isInitialized)throw new Error("SDK not initialized. Call initialize() first.");if(({Recorder:s}=await r._loadRemoteScript("recorder")),!s)throw new Error("Failed to load recorder script.");const{openImmediately:t,...n}={...r.config,...e},i="string"==typeof t?t:null;return s.initialize({...n,openImmediately:!i&&(t??!0)}),r.recorderRefCounter.update("increment"),window.addEventListener("pagehide",()=>{r.recorderRefCounter.update("decrement")}),i&&s.open(i,n),s}async function p(){if(!r.isInitialized||!r._loadRemoteScript)throw new Error("SDK not initialized. Call initialize() first.");const{Capture:e}=await r._loadRemoteScript("capture")??{};await(e?.initialize(r.config))}async function m(e){const t=`https://js.jam.dev/${e}.js`,n=await import(
1
+ function e(){const e=new EventTarget;return{addEventListener(t,n,r){e.addEventListener(t,n,r)},removeEventListener(t,n,r){e.removeEventListener(t,n,r)},dispatch:(t,n)=>e.dispatchEvent(new CustomEvent(t,{detail:n}))}}const t=(t,n=localStorage)=>{const r=`jam:${t}`,i=e();let o=null;try{o=n.getItem(r)}catch(e){}const a={count:Number.parseInt(o??"0",10)||0,addEventListener:i.addEventListener.bind(i),removeEventListener:i.removeEventListener.bind(i),update(e){const t=a.count;switch(e){case"increment":a.count+=1;break;case"decrement":a.count-=1;break;default:a.count=e}if(a.count<0&&(a.count=0),a.count!==t){try{n.setItem(r,`${a.count}`)}catch(e){}Object.assign(a,{count:a.count}),i.dispatch("update",a.count)}return a.count}};return window.addEventListener("storage",e=>{if(e.storageArea===n&&e.key===r){const t=Number.parseInt(e.newValue??"",10);Number.isNaN(t)||t===a.count||(a.count=t,i.dispatch("update",a.count))}}),a},n=e=>{try{const t=new URL(e),n={};for(const[e,r]of t.searchParams.entries())e.startsWith("jam-")&&(n[e]=r);return Object.keys(n).length>0?n:null}catch(e){}return null},r={isInitialized:!1},i=void 0,o=e(),a=o.addEventListener.bind(o),c=o.removeEventListener.bind(o),d=()=>r.isInitialized;let s=null;function u({recorderRefCounter:e=t("numRecorders"),_loadRemoteScript:i=m,...o}={}){if(r.isInitialized)throw new Error("SDK already initialized.");Object.assign(r,{isInitialized:!0,recorderRefCounter:e,config:o,_loadRemoteScript:i}),e.count>0?p():e.addEventListener("update",p,{once:!0}),window.addEventListener("popstate",c);const a={apply(e,t,n){const r=Reflect.apply(e,t,n);return c(),r}};function c(){if(s)return;const e=r.config?.parseJamData??n,t="string"==typeof r.config?.openImmediately?r.config.openImmediately:e(window.location.href)?.["jam-recording"];t&&l({openImmediately:!1!==r.config?.openImmediately&&t})}history.pushState=new Proxy(history.pushState,a),history.replaceState=new Proxy(history.replaceState,a),c()}async function l({...e}={}){if(s)return s;if(!r.isInitialized)throw new Error("SDK not initialized. Call initialize() first.");if(({Recorder:s}=await r._loadRemoteScript("recorder")),!s)throw new Error("Failed to load recorder script.");const{openImmediately:t,...n}={...r.config,...e},i="string"==typeof t?t:null;return s.initialize({...n,openImmediately:!i&&(t??!0)}),r.recorderRefCounter.update("increment"),window.addEventListener("pagehide",()=>{r.recorderRefCounter.update("decrement")}),i&&s.open(i,n),s}async function p(){if(!r.isInitialized||!r._loadRemoteScript)throw new Error("SDK not initialized. Call initialize() first.");const{Capture:e}=await r._loadRemoteScript("capture")??{};await(e?.initialize(r.config))}async function m(e){const t=`https://js.jam.dev/${e}.js`,n=await import(
2
2
  /* webpackIgnore: true */
3
3
  /* @vite-ignore */
4
4
  /* @rollup/plugin-dynamic-import-vars ignore */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jam.dev/recording-links",
3
- "version": "0.3.0-electron.5",
3
+ "version": "0.3.2",
4
4
  "description": "Capture bug reports from your users with the Jam recording links SDK",
5
5
  "keywords": [
6
6
  "jam",