@jam.dev/recording-links 0.1.1 → 0.2.0
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 +11 -4
- package/lib/__tests__/helpers/browser-mocks.d.ts +79 -0
- package/lib/__tests__/setup.d.ts +2 -0
- package/lib/sdk.d.ts +17 -12
- package/lib/sdk.js +2 -2
- package/lib/utils/refs.d.ts +1 -1
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -56,6 +56,7 @@ jam.initialize(); // Automatically loads recorder and opens recording "abc123"
|
|
|
56
56
|
jam.initialize({
|
|
57
57
|
teamId: "team-123,team-456", // Manually provide team ID(s); default: reads `<meta name="jam:team">` from the page
|
|
58
58
|
openImmediately: false, // Don't auto-open recorder from URL params
|
|
59
|
+
// OR: "recording-123" to auto-open a recording by ID
|
|
59
60
|
parseJamData: (href) => {
|
|
60
61
|
// Custom logic to extract recording data from URL
|
|
61
62
|
const url = new URL(href);
|
|
@@ -93,7 +94,7 @@ Initializes the SDK with optional configuration.
|
|
|
93
94
|
|
|
94
95
|
**Parameters:**
|
|
95
96
|
- `options.teamId?: string` - Team ID for recording validation
|
|
96
|
-
- `options.openImmediately?: boolean` - Whether to auto-open from URL (default: `true`)
|
|
97
|
+
- `options.openImmediately?: boolean | string` - Whether to auto-open from URL (default: `true`), or a recording ID string to open immediately
|
|
97
98
|
- `options.parseJamData?: (input: string) => SerializableJamData | null` - Custom URL parser
|
|
98
99
|
- `options.applyJamData?: (data: SerializableJamData) => string` - Custom URL applier
|
|
99
100
|
- `options.recorderRefCounter?: RefCounter` - Custom reference counter (advanced)
|
|
@@ -106,8 +107,7 @@ Initializes the SDK with optional configuration.
|
|
|
106
107
|
Loads the Jam recorder module and returns a promise that resolves to the recorder interface.
|
|
107
108
|
|
|
108
109
|
**Parameters:**
|
|
109
|
-
- `
|
|
110
|
-
- Plus all options from `InitializeOptions`
|
|
110
|
+
- `Omit<InitializeOptions, "recorderRefCounter">`
|
|
111
111
|
|
|
112
112
|
**Returns:**
|
|
113
113
|
- `Promise<RecorderSingleton>` - The Recorder singleton
|
|
@@ -115,6 +115,13 @@ Loads the Jam recorder module and returns a promise that resolves to the recorde
|
|
|
115
115
|
**Throws:**
|
|
116
116
|
- Error if SDK is not initialized
|
|
117
117
|
|
|
118
|
+
### `isInitialized()`
|
|
119
|
+
|
|
120
|
+
Returns whether the SDK has been initialized.
|
|
121
|
+
|
|
122
|
+
**Returns:**
|
|
123
|
+
- `boolean` - True if the SDK has been initialized, false otherwise
|
|
124
|
+
|
|
118
125
|
### `addEventListener(type, listener, options?)`
|
|
119
126
|
|
|
120
127
|
Add an event listener for SDK events.
|
|
@@ -159,7 +166,7 @@ type SerializableJamData = {
|
|
|
159
166
|
```typescript
|
|
160
167
|
type InitializeOptions = {
|
|
161
168
|
teamId?: string;
|
|
162
|
-
openImmediately?: boolean;
|
|
169
|
+
openImmediately?: boolean | string | undefined | null;
|
|
163
170
|
parseJamData?(input: string): SerializableJamData | null;
|
|
164
171
|
applyJamData?(data: SerializableJamData): string;
|
|
165
172
|
recorderRefCounter?: RefCounter;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
/**
|
|
3
|
+
* Shared browser mocking utilities for consistent test environment setup.
|
|
4
|
+
* This consolidates duplicate browser mock implementations across test files.
|
|
5
|
+
*/
|
|
6
|
+
export interface MockLocalStorage {
|
|
7
|
+
getItem: ReturnType<typeof vi.fn>;
|
|
8
|
+
setItem: ReturnType<typeof vi.fn>;
|
|
9
|
+
removeItem: ReturnType<typeof vi.fn>;
|
|
10
|
+
clear: ReturnType<typeof vi.fn>;
|
|
11
|
+
length: number;
|
|
12
|
+
key: ReturnType<typeof vi.fn>;
|
|
13
|
+
_store: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
export declare const createMockLocalStorage: () => MockLocalStorage;
|
|
16
|
+
export interface MockHistory {
|
|
17
|
+
pushState: ReturnType<typeof vi.fn>;
|
|
18
|
+
replaceState: ReturnType<typeof vi.fn>;
|
|
19
|
+
}
|
|
20
|
+
export declare const createMockHistory: () => MockHistory;
|
|
21
|
+
export interface MockWindow {
|
|
22
|
+
localStorage: MockLocalStorage;
|
|
23
|
+
location: {
|
|
24
|
+
href: string;
|
|
25
|
+
};
|
|
26
|
+
addEventListener: ReturnType<typeof vi.fn>;
|
|
27
|
+
removeEventListener: ReturnType<typeof vi.fn>;
|
|
28
|
+
history: MockHistory;
|
|
29
|
+
}
|
|
30
|
+
export declare const createMockWindow: (initialUrl?: string) => MockWindow;
|
|
31
|
+
export declare class MockPopStateEvent {
|
|
32
|
+
type: string;
|
|
33
|
+
constructor(type: string, options?: PopStateEventInit);
|
|
34
|
+
state: any;
|
|
35
|
+
}
|
|
36
|
+
export declare class MockCustomEvent {
|
|
37
|
+
type: string;
|
|
38
|
+
options: {
|
|
39
|
+
detail?: any;
|
|
40
|
+
};
|
|
41
|
+
constructor(type: string, options?: {
|
|
42
|
+
detail?: any;
|
|
43
|
+
});
|
|
44
|
+
detail: any;
|
|
45
|
+
}
|
|
46
|
+
export declare class MockEventTarget {
|
|
47
|
+
private listeners;
|
|
48
|
+
addEventListener(type: string, listener: EventListener, options?: boolean | AddEventListenerOptions): void;
|
|
49
|
+
removeEventListener(type: string, listener: EventListener, options?: boolean | AddEventListenerOptions): void;
|
|
50
|
+
dispatchEvent(event: CustomEvent): boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Complete browser environment mock setup
|
|
54
|
+
*/
|
|
55
|
+
export interface BrowserMocks {
|
|
56
|
+
localStorage: MockLocalStorage;
|
|
57
|
+
window: MockWindow;
|
|
58
|
+
history: MockHistory;
|
|
59
|
+
resetMocks: () => void;
|
|
60
|
+
}
|
|
61
|
+
export declare const createBrowserMocks: (initialUrl?: string) => BrowserMocks;
|
|
62
|
+
/**
|
|
63
|
+
* Helper to set test URL with parameters
|
|
64
|
+
*/
|
|
65
|
+
export declare const setTestUrl: (params?: Record<string, string>, baseUrl?: string) => string;
|
|
66
|
+
/**
|
|
67
|
+
* Helper to create mock RefCounter for tests
|
|
68
|
+
*/
|
|
69
|
+
export declare const createMockRefCounter: (count?: number) => {
|
|
70
|
+
count: number;
|
|
71
|
+
update: import("vitest").Mock<(...args: any[]) => any>;
|
|
72
|
+
addEventListener: import("vitest").Mock<(...args: any[]) => any>;
|
|
73
|
+
removeEventListener: import("vitest").Mock<(...args: any[]) => any>;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Browser environment setup helper that combines environment reset with mock creation
|
|
77
|
+
*/
|
|
78
|
+
export declare const setupBrowserEnvironment: (initialUrl?: string) => MockHistory;
|
|
79
|
+
//# sourceMappingURL=browser-mocks.d.ts.map
|
package/lib/sdk.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ type Events = {
|
|
|
10
10
|
name: "recorder";
|
|
11
11
|
Recorder: RecorderSingleton;
|
|
12
12
|
} | {
|
|
13
|
-
name:
|
|
13
|
+
name: "capture";
|
|
14
14
|
});
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
@@ -48,9 +48,11 @@ type InitializeOptions = {
|
|
|
48
48
|
teamId?: string;
|
|
49
49
|
/**
|
|
50
50
|
* Whether to open the JamRecorder immediately on initialization.
|
|
51
|
+
* May be provided a recording ID as a string, in which case we will open it.
|
|
52
|
+
* If the value resolves to `undefined` or `null`, we will default to `true`.
|
|
51
53
|
* Default: `true`
|
|
52
54
|
*/
|
|
53
|
-
openImmediately?: boolean;
|
|
55
|
+
openImmediately?: boolean | string | undefined | null;
|
|
54
56
|
/**
|
|
55
57
|
* Extract a `recordingId` and `jamTitle` from the provided string.
|
|
56
58
|
* Defaults to reading the `jam-recording` and `jam-title` QSPs off the URL.
|
|
@@ -62,6 +64,8 @@ type InitializeOptions = {
|
|
|
62
64
|
* Setting a custom `parseJamData` disables this default.
|
|
63
65
|
*/
|
|
64
66
|
applyJamData?(data: SerializableJamData): string;
|
|
67
|
+
/** Selectors to blur on host pages while the Jam Recorder is recording. */
|
|
68
|
+
blurSelectors?: string | string[] | (() => string | string[]);
|
|
65
69
|
};
|
|
66
70
|
/**
|
|
67
71
|
* Data structure representing Jam recording metadata that can be serialized to/from URLs.
|
|
@@ -72,6 +76,9 @@ export type SerializableJamData = {
|
|
|
72
76
|
/** The human-readable title of the recording, or null if not present */
|
|
73
77
|
jamTitle: string | null;
|
|
74
78
|
};
|
|
79
|
+
declare function resetForTesting(): void;
|
|
80
|
+
/** @internal - Reset SDK state for testing (only available in dev builds) */
|
|
81
|
+
export declare const _resetForTesting: typeof resetForTesting | undefined;
|
|
75
82
|
/**
|
|
76
83
|
* Add an event listener for SDK events.
|
|
77
84
|
* @param type - The event type to listen for
|
|
@@ -86,6 +93,8 @@ export declare const addEventListener: <K extends "loaded">(type: K, listener: (
|
|
|
86
93
|
* @param options - Optional event listener configuration
|
|
87
94
|
*/
|
|
88
95
|
export declare const removeEventListener: <K extends "loaded">(type: K, listener: (event: CustomEvent<Events[K]>) => void, options?: boolean | AddEventListenerOptions) => void;
|
|
96
|
+
/** Whether the SDK has been initialized. */
|
|
97
|
+
export declare const isInitialized: () => false;
|
|
89
98
|
/**
|
|
90
99
|
* The loaded Recorder singleton instance. Will be null until loadRecorder() is called successfully.
|
|
91
100
|
*/
|
|
@@ -101,6 +110,7 @@ export declare let Recorder: RecorderSingleton | null;
|
|
|
101
110
|
* @param options.recorderRefCounter - Custom reference counter for managing recorder instances across tabs
|
|
102
111
|
* @param options.teamId - Team ID for validating recording IDs
|
|
103
112
|
* @param options.openImmediately - Whether to automatically open recorder from URL parameters (default: true)
|
|
113
|
+
* Can be set to a specific recording ID as a string to open that recording.
|
|
104
114
|
* @param options.parseJamData - Custom function to extract recording data from URLs
|
|
105
115
|
* @param options.applyJamData - Custom function to apply recording data to URLs
|
|
106
116
|
*
|
|
@@ -114,11 +124,11 @@ export declare let Recorder: RecorderSingleton | null;
|
|
|
114
124
|
* // Custom configuration
|
|
115
125
|
* jam.initialize({
|
|
116
126
|
* teamId: "team-123",
|
|
117
|
-
* openImmediately: false
|
|
127
|
+
* openImmediately: false // or "abc-123" to open a specific recording
|
|
118
128
|
* });
|
|
119
129
|
* ```
|
|
120
130
|
*/
|
|
121
|
-
export declare function initialize({ recorderRefCounter, ...config }?: InitializeOptions): void;
|
|
131
|
+
export declare function initialize({ recorderRefCounter, _loadRemoteScript, ...config }?: InitializeOptions): void;
|
|
122
132
|
/**
|
|
123
133
|
* Loads the Jam recorder module and returns the recorder singleton.
|
|
124
134
|
*
|
|
@@ -127,9 +137,9 @@ export declare function initialize({ recorderRefCounter, ...config }?: Initializ
|
|
|
127
137
|
* for cross-tab coordination.
|
|
128
138
|
*
|
|
129
139
|
* @param options - Configuration options for loading the recorder
|
|
130
|
-
* @param options.recordingId - Optional recording ID to open immediately after loading
|
|
131
140
|
* @param options.teamId - Team ID for validating recording IDs
|
|
132
141
|
* @param options.openImmediately - Whether to automatically open the recorder (default: true)
|
|
142
|
+
* Can be set to a specific recording ID as a string to open that recording.
|
|
133
143
|
* @param options.parseJamData - Custom function to extract recording data from URLs
|
|
134
144
|
* @param options.applyJamData - Custom function to apply recording data to URLs
|
|
135
145
|
*
|
|
@@ -145,18 +155,13 @@ export declare function initialize({ recorderRefCounter, ...config }?: Initializ
|
|
|
145
155
|
*
|
|
146
156
|
* // Load recorder and open a specific recording
|
|
147
157
|
* const recorder = await jam.loadRecorder({
|
|
148
|
-
*
|
|
158
|
+
* openImmediately: false, // or "recording-abc123"
|
|
149
159
|
* });
|
|
150
160
|
*
|
|
151
161
|
* // Use the recorder
|
|
152
162
|
* recorder.open("recording-xyz789", { jamTitle: "Bug Report" });
|
|
153
163
|
* ```
|
|
154
164
|
*/
|
|
155
|
-
export declare function loadRecorder({
|
|
156
|
-
/**
|
|
157
|
-
* An optional recording ID; if provided, a recorder will to open the recorder with.
|
|
158
|
-
*/
|
|
159
|
-
recordingId?: string | null;
|
|
160
|
-
}): Promise<RecorderSingleton>;
|
|
165
|
+
export declare function loadRecorder({ ...config }?: Omit<InitializeOptions, "recorderRefCounter">): Promise<RecorderSingleton>;
|
|
161
166
|
export {};
|
|
162
167
|
//# sourceMappingURL=sdk.d.ts.map
|
package/lib/sdk.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
function e(){const e=new EventTarget;return{addEventListener(t,r
|
|
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(
|
|
2
2
|
/* webpackIgnore: true */
|
|
3
3
|
/* @vite-ignore */
|
|
4
4
|
/* @rollup/plugin-dynamic-import-vars ignore */
|
|
5
|
-
t);if("recorder"===e){if(!
|
|
5
|
+
t);if("recorder"===e){if(!n.Recorder)throw new Error("Loaded recorder script, but Recorder not found.");o.dispatch("loaded",{type:"script",name:e,Recorder:n.Recorder})}else o.dispatch("loaded",{type:"script",name:e});return n}export{s as Recorder,i as _resetForTesting,a as addEventListener,u as initialize,d as isInitialized,l as loadRecorder,c as removeEventListener};//# sourceMappingURL=sdk.js.map
|
package/lib/utils/refs.d.ts
CHANGED
|
@@ -5,5 +5,5 @@ export type RefCounter = Omit<events.Emitter<{
|
|
|
5
5
|
count: number;
|
|
6
6
|
update(value: "increment" | "decrement" | number): number;
|
|
7
7
|
};
|
|
8
|
-
export declare const
|
|
8
|
+
export declare const createStorageRefCounter: <K extends string>(name: K, storage?: Storage) => RefCounter;
|
|
9
9
|
//# sourceMappingURL=refs.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jam.dev/recording-links",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Capture bug reports from your users with the Jam recording links SDK",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jam",
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
"./sdk": {
|
|
26
26
|
"types": "./lib/sdk.d.ts",
|
|
27
27
|
"import": "./lib/sdk.js"
|
|
28
|
+
},
|
|
29
|
+
"./lib/sdk": {
|
|
30
|
+
"types": "./lib/sdk.d.ts",
|
|
31
|
+
"import": "./lib/sdk.js"
|
|
28
32
|
}
|
|
29
33
|
},
|
|
30
34
|
"files": [
|
|
@@ -39,11 +43,11 @@
|
|
|
39
43
|
"dev:remote": "rollup -c --watch --environment BUILD:dev,JAM_JS_ORIGIN:prod",
|
|
40
44
|
"dev:staging": "rollup -c --watch --environment BUILD:dev,JAM_JS_ORIGIN:staging",
|
|
41
45
|
"build": "rollup -c --environment BUILD:prod",
|
|
42
|
-
"prepublishOnly": "bun run scripts/validate-build.ts",
|
|
43
46
|
"build:dev": "rollup -c --environment BUILD:dev",
|
|
47
|
+
"prepublishOnly": "bun run scripts/validate-build.ts",
|
|
44
48
|
"lint": "biome lint './src' --diagnostic-level=error",
|
|
45
49
|
"lint:fix": "biome lint './src' --diagnostic-level=error --fix",
|
|
46
|
-
"test": "vitest --passWithNoTests",
|
|
50
|
+
"test": "vitest --passWithNoTests src/",
|
|
47
51
|
"type-check": "tsc --noEmit"
|
|
48
52
|
},
|
|
49
53
|
"dependencies": {},
|
|
@@ -54,6 +58,7 @@
|
|
|
54
58
|
"@rollup/plugin-typescript": "^11.1.6",
|
|
55
59
|
"@types/node": "^20.0.0",
|
|
56
60
|
"biome": "^0.3.3",
|
|
61
|
+
"jsdom": "^27.0.0",
|
|
57
62
|
"rollup": "^4.12.0",
|
|
58
63
|
"tslib": "^2.8.1",
|
|
59
64
|
"typescript": "^5.8.2",
|