@sailfish-ai/recorder 1.10.13 → 1.11.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 +309 -7
- package/dist/babel-plugin-sailfish-source.cjs.br +0 -0
- package/dist/babel-plugin-sailfish-source.cjs.gz +0 -0
- package/dist/babel-plugin-sailfish-source.js.br +0 -0
- package/dist/babel-plugin-sailfish-source.js.gz +0 -0
- package/dist/chunkSerializer.js.br +0 -0
- package/dist/chunkSerializer.js.gz +0 -0
- package/dist/chunks/{chunkSerializer-CodMnuS3.js → chunkSerializer-CRDpgzTs.js} +1 -1
- package/dist/chunks/chunkSerializer-CRDpgzTs.js.br +0 -0
- package/dist/chunks/chunkSerializer-CRDpgzTs.js.gz +0 -0
- package/dist/chunks/{chunkSerializer-Dk1eF3S8.js → chunkSerializer-ZzIoYlP2.js} +1 -1
- package/dist/chunks/chunkSerializer-ZzIoYlP2.js.br +0 -0
- package/dist/chunks/chunkSerializer-ZzIoYlP2.js.gz +0 -0
- package/dist/chunks/{index-DW416eVj.js → index-BQn1Q-2-.js} +36 -32
- package/dist/chunks/index-BQn1Q-2-.js.br +0 -0
- package/dist/chunks/index-BQn1Q-2-.js.gz +0 -0
- package/dist/chunks/{index-DvLh2k6O.js → index-Dq_tjmkZ.js} +30 -26
- package/dist/chunks/index-Dq_tjmkZ.js.br +0 -0
- package/dist/chunks/index-Dq_tjmkZ.js.gz +0 -0
- package/dist/constants.js.br +0 -0
- package/dist/constants.js.gz +0 -0
- package/dist/deviceInfo.js.br +0 -0
- package/dist/deviceInfo.js.gz +0 -0
- package/dist/env.js.br +0 -0
- package/dist/env.js.gz +0 -0
- package/dist/errorInterceptor.js.br +0 -0
- package/dist/errorInterceptor.js.gz +0 -0
- package/dist/eventStore.js.br +0 -0
- package/dist/eventStore.js.gz +0 -0
- package/dist/exponentialBackoff.js.br +0 -0
- package/dist/exponentialBackoff.js.gz +0 -0
- package/dist/fiberHook.js.br +0 -0
- package/dist/fiberHook.js.gz +0 -0
- package/dist/frameworkDetection.js.br +0 -0
- package/dist/frameworkDetection.js.gz +0 -0
- package/dist/graphql.js.br +0 -0
- package/dist/graphql.js.gz +0 -0
- package/dist/headlessDetection.js.br +0 -0
- package/dist/headlessDetection.js.gz +0 -0
- package/dist/inAppReportIssueModal/index.js.br +0 -0
- package/dist/inAppReportIssueModal/index.js.gz +0 -0
- package/dist/inAppReportIssueModal/integrations.js.br +0 -0
- package/dist/inAppReportIssueModal/integrations.js.gz +0 -0
- package/dist/inAppReportIssueModal/state.js.br +0 -0
- package/dist/inAppReportIssueModal/state.js.gz +0 -0
- package/dist/inAppReportIssueModal/ui.js.br +0 -0
- package/dist/inAppReportIssueModal/ui.js.gz +0 -0
- package/dist/index.js +44 -34
- package/dist/index.js.br +0 -0
- package/dist/index.js.gz +0 -0
- package/dist/notifyEventStore.js.br +0 -0
- package/dist/notifyEventStore.js.gz +0 -0
- package/dist/recorder.cjs +1 -1
- package/dist/recorder.cjs.br +0 -0
- package/dist/recorder.cjs.gz +0 -0
- package/dist/recorder.js +1 -1
- package/dist/recorder.js.br +0 -0
- package/dist/recorder.js.gz +0 -0
- package/dist/recorder.umd.cjs +8883 -0
- package/dist/recorder.umd.cjs.br +0 -0
- package/dist/recorder.umd.cjs.gz +0 -0
- package/dist/recording.js.br +0 -0
- package/dist/recording.js.gz +0 -0
- package/dist/segmentHelpers.js.br +0 -0
- package/dist/segmentHelpers.js.gz +0 -0
- package/dist/sendSailfishMessages.js.br +0 -0
- package/dist/sendSailfishMessages.js.gz +0 -0
- package/dist/session.js.br +0 -0
- package/dist/session.js.gz +0 -0
- package/dist/snippet-auto-init.js +44 -0
- package/dist/snippet-auto-init.js.br +0 -0
- package/dist/snippet-auto-init.js.gz +0 -0
- package/dist/sourceLocation.js.br +0 -0
- package/dist/sourceLocation.js.gz +0 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/snippet-auto-init.d.ts +1 -0
- package/dist/types/umd-entry.d.ts +9 -0
- package/dist/umd-entry.js +11 -0
- package/dist/utils.js.br +0 -0
- package/dist/utils.js.gz +0 -0
- package/dist/uuid.js.br +0 -0
- package/dist/uuid.js.gz +0 -0
- package/dist/websocket.js.br +0 -0
- package/dist/websocket.js.gz +0 -0
- package/package.json +9 -5
- package/dist/chunks/chunkSerializer-CodMnuS3.js.br +0 -0
- package/dist/chunks/chunkSerializer-CodMnuS3.js.gz +0 -0
- package/dist/chunks/chunkSerializer-Dk1eF3S8.js.br +0 -0
- package/dist/chunks/chunkSerializer-Dk1eF3S8.js.gz +0 -0
- package/dist/chunks/index-DW416eVj.js.br +0 -0
- package/dist/chunks/index-DW416eVj.js.gz +0 -0
- package/dist/chunks/index-DvLh2k6O.js.br +0 -0
- package/dist/chunks/index-DvLh2k6O.js.gz +0 -0
package/README.md
CHANGED
|
@@ -1,12 +1,314 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @sailfish-ai/recorder
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Frontend session recorder for [Sailfish](https://app.sailfishqa.com). Captures the DOM, console, network activity, errors, and user interactions so replay is available in the dashboard alongside the backend traces Sailfish already collects.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Works in any browser JavaScript or TypeScript app — React, Vue, Angular, Next.js, Nuxt, Svelte, Shopify, plain HTML. Ships UMD, ESM, and CJS builds; can be installed via npm or loaded as a single `<script>` tag from a CDN.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **Signup / dashboard:** https://app.sailfishqa.com
|
|
8
|
+
- **Source:** [github.com/SailfishAI/sailfish](https://github.com/SailfishAI/sailfish) (internal)
|
|
8
9
|
|
|
9
|
-
##
|
|
10
|
+
## Installation
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
```bash
|
|
13
|
+
npm install @sailfish-ai/recorder
|
|
14
|
+
# or
|
|
15
|
+
yarn add @sailfish-ai/recorder
|
|
16
|
+
# or
|
|
17
|
+
pnpm add @sailfish-ai/recorder
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
### ES modules / bundler (Vite, Webpack, Rollup, Next.js, etc.)
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { initRecorder } from "@sailfish-ai/recorder";
|
|
26
|
+
|
|
27
|
+
// SSR guard — don't touch browser storage during server render.
|
|
28
|
+
if (typeof window !== "undefined") {
|
|
29
|
+
initRecorder({ apiKey: "YOUR_API_KEY" });
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### CDN (single `<script>` tag, auto-init)
|
|
34
|
+
|
|
35
|
+
The UMD build auto-initializes from `data-*` attributes on the script tag — zero JavaScript needed on your side:
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<script
|
|
39
|
+
src="https://cdn.jsdelivr.net/npm/@sailfish-ai/recorder@1/dist/recorder.umd.cjs"
|
|
40
|
+
data-api-key="YOUR_API_KEY"
|
|
41
|
+
data-service-identifier="my-app"
|
|
42
|
+
data-service-version="1.0.0"
|
|
43
|
+
crossorigin="anonymous"
|
|
44
|
+
></script>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Drop that into the `<head>` of any HTML page (Shopify, Webflow, WordPress, static sites, server-rendered templates) and recording starts automatically. Errors inside the recorder are swallowed — Sailfish never breaks the host page.
|
|
48
|
+
|
|
49
|
+
### CDN (manual init)
|
|
50
|
+
|
|
51
|
+
If you'd rather call `initRecorder` yourself — for example to pass runtime options — omit `data-api-key` and use the `SailfishRecorder` global:
|
|
52
|
+
|
|
53
|
+
```html
|
|
54
|
+
<script
|
|
55
|
+
src="https://cdn.jsdelivr.net/npm/@sailfish-ai/recorder@1/dist/recorder.umd.cjs"
|
|
56
|
+
crossorigin="anonymous"
|
|
57
|
+
></script>
|
|
58
|
+
<script>
|
|
59
|
+
window.SailfishRecorder.initRecorder({
|
|
60
|
+
apiKey: "YOUR_API_KEY",
|
|
61
|
+
serviceIdentifier: "my-app",
|
|
62
|
+
serviceVersion: "1.0.0",
|
|
63
|
+
});
|
|
64
|
+
</script>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## CDN delivery and bundle size
|
|
68
|
+
|
|
69
|
+
`@sailfish-ai/recorder` is published publicly on npm, so any major JavaScript CDN can serve it — jsDelivr, unpkg, esm.sh, esm.run, jspm. The UMD bundle is self-contained (no external dependencies to resolve) and exposes the `SailfishRecorder` global.
|
|
70
|
+
|
|
71
|
+
### What the browser actually downloads
|
|
72
|
+
|
|
73
|
+
The UMD file on disk is ~473 KB, but browsers never see that number. CDNs do HTTP content-negotiated compression: the browser advertises `Accept-Encoding: gzip, br` automatically, and the CDN returns the file with a `Content-Encoding: br` (or `gzip`) header. The browser decompresses on arrival. **No configuration needed by the site author — just include the `<script>` tag.**
|
|
74
|
+
|
|
75
|
+
| Encoding (delivered) | Size on the wire | Browsers |
|
|
76
|
+
|---|---:|---|
|
|
77
|
+
| **Brotli** (`br`) | **~84 KB** | Chrome, Firefox, Safari, Edge (all current versions) |
|
|
78
|
+
| **Gzip** (`gzip`) | **~117 KB** | Older browsers, curl without `--compressed`, crawlers |
|
|
79
|
+
| Uncompressed | ~473 KB | Only if the client explicitly sends `Accept-Encoding: identity` |
|
|
80
|
+
|
|
81
|
+
jsDelivr and unpkg set `Vary: Accept-Encoding`, so each encoding is cached independently at the edge — the first user per encoding per region pays a small compression cost, every subsequent request is served straight from the cache.
|
|
82
|
+
|
|
83
|
+
### CDN options
|
|
84
|
+
|
|
85
|
+
| CDN | URL pattern | Notes |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| **jsDelivr** (recommended) | `https://cdn.jsdelivr.net/npm/@sailfish-ai/recorder@1/dist/recorder.umd.cjs` | Fastest p50 in our load tests, brotli over HTTP/2, global anycast. Cache stays warm across versions. |
|
|
88
|
+
| **unpkg** | `https://unpkg.com/@sailfish-ai/recorder@1/dist/recorder.umd.cjs` | Cloudflare-backed, also fast. Good fallback. |
|
|
89
|
+
| **esm.run** / **jsDelivr `+esm`** | `https://esm.run/@sailfish-ai/recorder@1` | Single-file ESM for `<script type="module">`. Pre-bundled (~31 KB brotli over-wire). |
|
|
90
|
+
| **esm.sh** | `https://esm.sh/@sailfish-ai/recorder@1` | ESM with separate dependency module graph (multiple HTTP requests — slower first load). |
|
|
91
|
+
|
|
92
|
+
Version pinning:
|
|
93
|
+
|
|
94
|
+
- `@sailfish-ai/recorder@1` — track the latest `1.x` (recommended; gets bug fixes).
|
|
95
|
+
- `@sailfish-ai/recorder@1.10.11` — pin exactly (longest CDN cache TTL; use for absolute stability).
|
|
96
|
+
|
|
97
|
+
### Which build format should I use?
|
|
98
|
+
|
|
99
|
+
| You're building… | Use | How |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| Plain HTML, Shopify, Webflow, WordPress, server-rendered template | **UMD via CDN** | `<script src="…/recorder.umd.cjs" data-api-key="…">` |
|
|
102
|
+
| React / Vue / Next.js / Svelte app with a bundler | **ESM via npm** | `import { initRecorder } from "@sailfish-ai/recorder"` |
|
|
103
|
+
| Modern browser app with native ESM (no bundler) | **ESM via CDN** | `<script type="module">import { initRecorder } from "https://esm.run/@sailfish-ai/recorder@1"` |
|
|
104
|
+
| Node.js CommonJS (unusual — this is a browser package) | **CJS via npm** | `const { initRecorder } = require("@sailfish-ai/recorder")` |
|
|
105
|
+
|
|
106
|
+
## API
|
|
107
|
+
|
|
108
|
+
All functions are top-level exports of `@sailfish-ai/recorder`. On the CDN build they're also available as `window.SailfishRecorder.*`.
|
|
109
|
+
|
|
110
|
+
### `initRecorder(options)`
|
|
111
|
+
|
|
112
|
+
Initialize the recorder. Returns `Promise<void>`. Safe to call once per page load; concurrent calls are coalesced.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
await initRecorder({
|
|
116
|
+
apiKey: "YOUR_API_KEY", // required
|
|
117
|
+
serviceIdentifier: "my-app", // optional — your app name in the dashboard
|
|
118
|
+
serviceVersion: "1.0.0", // optional — your app version for filtering sessions
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### Options
|
|
123
|
+
|
|
124
|
+
| Field | Type | Default | Purpose |
|
|
125
|
+
|---|---|---|---|
|
|
126
|
+
| `apiKey` | `string` | **required** | Your Sailfish API key (from the dashboard). |
|
|
127
|
+
| `backendApi` | `string` | `https://api-service.sailfishqa.com` | Backend endpoint. Override for self-hosted Sailfish deployments. |
|
|
128
|
+
| `serviceIdentifier` | `string` | — | Application name shown in the dashboard. |
|
|
129
|
+
| `serviceVersion` | `string` | — | Application version, used for session filtering. |
|
|
130
|
+
| `gitSha` | `string` | auto-detected | Build commit SHA for session filtering. |
|
|
131
|
+
| `serviceAdditionalMetadata` | `Record<string, any>` | — | Arbitrary metadata attached to every session. |
|
|
132
|
+
| `domainsToPropagateHeaderTo` | `string[]` | `[]` | Domains (wildcards allowed) that receive the `X-Sf3-Rid` tracing header, letting Sailfish join frontend sessions with backend traces. |
|
|
133
|
+
| `domainsToNotPropagateHeaderTo` | `string[]` | `[]` | Domains to exclude, appended to the built-in denylist. |
|
|
134
|
+
| `enableFiberTracking` | `boolean` | `false` | Adds `data-sf-source="file.tsx:42"` attributes to DOM nodes for source-mapped coverage (React only). Pairs with the Babel plugin. |
|
|
135
|
+
| `enableIpTracking` | `boolean` | `false` | Fetches the visitor IP asynchronously for session metadata. |
|
|
136
|
+
| `captureStreamingResponseBody` | `boolean` | `true` | Capture prefixes of streaming (SSE, ndjson, chunked) responses. |
|
|
137
|
+
| `captureResponseBodyMaxMb` | `number` | `10` | Max non-streaming response body to capture, in MB. `0` disables body capture. |
|
|
138
|
+
| `captureStreamPrefixKb` | `number` | `64` | Max streaming-body prefix to capture, in KB. |
|
|
139
|
+
| `captureStreamTimeoutMs` | `number` | `10000` | Timeout for reading streaming bodies, in ms. |
|
|
140
|
+
| `deferRecording` | `boolean` | `true` | Defers the initial DOM snapshot until after first paint / idle. |
|
|
141
|
+
| `chunkSnapshot` | `boolean` | `false` | Yield to the browser every 500 nodes during the initial snapshot (smoother on very large pages). |
|
|
142
|
+
| `useWsWorker` | `boolean` | `true` | Run the WebSocket sender in a Web Worker. Disable if your CSP blocks `worker-src blob:`. |
|
|
143
|
+
| `reportIssueShortcuts` | `ShortcutsConfig` | — | Custom keyboard shortcuts for the report-issue modal. |
|
|
144
|
+
| `showEngTicketFieldsInReportIssueModalDefault` | `boolean` | `false` | Pre-expand Jira / Linear fields in the in-app issue modal. |
|
|
145
|
+
|
|
146
|
+
### `identify(userId, traits?, overwrite?)`
|
|
147
|
+
|
|
148
|
+
Tag the current session with a user identity. Subsequent calls with the same `userId` are deduplicated.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
identify("user_abc123", { email: "dana@example.com", plan: "pro" });
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### `addOrUpdateMetadata(metadata)`
|
|
155
|
+
|
|
156
|
+
Attach arbitrary metadata to the current session. Values are merged with any previously-set metadata.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
addOrUpdateMetadata({ experiment: "pricing-v2", cohort: "B" });
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### `getOrSetSessionId()`
|
|
163
|
+
|
|
164
|
+
Return the current session ID (generates one if none exists). Useful for correlating logs on your side with Sailfish recordings.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
const sessionId = getOrSetSessionId();
|
|
168
|
+
console.log("Sailfish session:", sessionId);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### `openReportIssueModal(options?)`
|
|
172
|
+
|
|
173
|
+
Open the in-app bug-report modal. If `showEngTicketFields: true` is passed (or set at init), Jira / Linear fields are shown by default.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
openReportIssueModal({ showEngTicketFields: true });
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### `enableFunctionSpanTracking()` / `disableFunctionSpanTracking()` / `isFunctionSpanTrackingEnabled()`
|
|
180
|
+
|
|
181
|
+
Toggle per-session backend function-span tracing at runtime.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
if (isFunctionSpanTrackingEnabled()) disableFunctionSpanTracking();
|
|
185
|
+
enableFunctionSpanTracking();
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Framework examples
|
|
189
|
+
|
|
190
|
+
### React / Vite
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
import { useEffect } from "react";
|
|
194
|
+
import { initRecorder } from "@sailfish-ai/recorder";
|
|
195
|
+
|
|
196
|
+
export function App() {
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
initRecorder({ apiKey: import.meta.env.VITE_SAILFISH_KEY });
|
|
199
|
+
}, []);
|
|
200
|
+
return <Routes />;
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Next.js (App Router)
|
|
205
|
+
|
|
206
|
+
`'use client'` is required — `initRecorder` touches `localStorage` and must run in the browser.
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
"use client";
|
|
210
|
+
import { useEffect } from "react";
|
|
211
|
+
import { initRecorder } from "@sailfish-ai/recorder";
|
|
212
|
+
|
|
213
|
+
export function SailfishProvider({ children }: { children: React.ReactNode }) {
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
initRecorder({ apiKey: process.env.NEXT_PUBLIC_SAILFISH_KEY! });
|
|
216
|
+
}, []);
|
|
217
|
+
return <>{children}</>;
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Place `<SailfishProvider>` in `app/layout.tsx`.
|
|
222
|
+
|
|
223
|
+
### Plain HTML / Shopify / Webflow / WordPress
|
|
224
|
+
|
|
225
|
+
```html
|
|
226
|
+
<script
|
|
227
|
+
src="https://cdn.jsdelivr.net/npm/@sailfish-ai/recorder@1/dist/recorder.umd.cjs"
|
|
228
|
+
data-api-key="YOUR_API_KEY"
|
|
229
|
+
crossorigin="anonymous"
|
|
230
|
+
></script>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Babel plugin — source-mapped coverage
|
|
234
|
+
|
|
235
|
+
`@sailfish-ai/recorder/babel-plugin` injects `data-sf-source="file.tsx:42"` attributes into your JSX at compile time so clicks and views in the Sailfish dashboard link back to the exact source line.
|
|
236
|
+
|
|
237
|
+
### Vite + React
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
// vite.config.ts
|
|
241
|
+
import { defineConfig } from "vite";
|
|
242
|
+
import react from "@vitejs/plugin-react";
|
|
243
|
+
import sailfishSourcePlugin from "@sailfish-ai/recorder/babel-plugin";
|
|
244
|
+
|
|
245
|
+
export default defineConfig({
|
|
246
|
+
plugins: [
|
|
247
|
+
react({ babel: { plugins: [sailfishSourcePlugin] } }),
|
|
248
|
+
],
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Babel / CRA / Next.js
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
// babel.config.js
|
|
256
|
+
const sailfishSourcePlugin = require("@sailfish-ai/recorder/babel-plugin");
|
|
257
|
+
module.exports = {
|
|
258
|
+
plugins: [[sailfishSourcePlugin, { allElements: false }]],
|
|
259
|
+
};
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Pass `{ allElements: true }` to annotate every element (default annotates only interactive ones — buttons, inputs, links, form controls).
|
|
263
|
+
|
|
264
|
+
## Configuration recipes
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
// Self-hosted Sailfish backend
|
|
268
|
+
initRecorder({
|
|
269
|
+
apiKey: "YOUR_KEY",
|
|
270
|
+
backendApi: "https://sailfish.your-company.com",
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Join frontend sessions with backend traces across your API
|
|
274
|
+
initRecorder({
|
|
275
|
+
apiKey: "YOUR_KEY",
|
|
276
|
+
domainsToPropagateHeaderTo: ["api.mycompany.com", "*.mycompany.com"],
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Strict CSP (no blob: workers)
|
|
280
|
+
initRecorder({
|
|
281
|
+
apiKey: "YOUR_KEY",
|
|
282
|
+
useWsWorker: false,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Capture more of streaming LLM response bodies
|
|
286
|
+
initRecorder({
|
|
287
|
+
apiKey: "YOUR_KEY",
|
|
288
|
+
captureStreamingResponseBody: true,
|
|
289
|
+
captureStreamPrefixKb: 256,
|
|
290
|
+
captureStreamTimeoutMs: 30_000,
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## SSR / Next.js note
|
|
295
|
+
|
|
296
|
+
`initRecorder` touches `localStorage`, `sessionStorage`, and `window` — none of which exist during server-side rendering. Always guard with `typeof window !== "undefined"` or call from `useEffect` / `onMount` / client-only equivalents.
|
|
297
|
+
|
|
298
|
+
Without a guard, frameworks like Next.js (on Vercel) will fail at build/SSR with:
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
ReferenceError: localStorage is not defined
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Troubleshooting
|
|
305
|
+
|
|
306
|
+
- **`window.SailfishRecorder` is undefined after the `<script>` loads.** You're probably using an ESM URL (`esm.run`, `esm.sh`) with a non-module `<script>`. Either use the UMD URL (`/dist/recorder.umd.cjs`) or add `type="module"` to the tag.
|
|
307
|
+
- **`Refused to create a worker from 'blob:...'` in the console.** Your Content-Security-Policy blocks blob workers. Either allow `worker-src blob:` or init with `useWsWorker: false`.
|
|
308
|
+
- **Recordings never reach the dashboard.** Verify the `apiKey` matches the project in the Sailfish dashboard, and that your ad-blocker isn't blocking `api-service.sailfishqa.com`. Open your browser devtools network tab and filter by `sailfish`.
|
|
309
|
+
- **SSR error: `localStorage is not defined`.** See the SSR note above — `initRecorder` must run in a browser-only code path.
|
|
310
|
+
- **Mixed-content warnings.** All Sailfish CDN and API URLs are HTTPS; make sure your page also serves over HTTPS.
|
|
311
|
+
|
|
312
|
+
## License
|
|
313
|
+
|
|
314
|
+
Proprietary. See [sailfishqa.com/terms](https://sailfishqa.com) for terms of service.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const e = require("./index-
|
|
3
|
+
const e = require("./index-BQn1Q-2-.js");
|
|
4
4
|
exports.chunkedSnapshot = async function chunkedSnapshot(t, n, o = {}) {
|
|
5
5
|
const s = o.chunkSize ?? 500, r = o.maxChunkMs ?? 16, { blockClass: c, blockSelector: a, maskTextClass: i, maskTextSelector: d } = o;
|
|
6
6
|
let u = 100001, l = 0, N = performance.now();
|
|
Binary file
|
|
Binary file
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { y as e } from "./index-
|
|
1
|
+
import { y as e } from "./index-Dq_tjmkZ.js";
|
|
2
2
|
async function chunkedSnapshot(t, n, o = {}) {
|
|
3
3
|
const s = o.chunkSize ?? 500, r = o.maxChunkMs ?? 16, { blockClass: c, blockSelector: a, maskTextClass: i, maskTextSelector: d } = o;
|
|
4
4
|
let u = 100001, l = 0, N = performance.now();
|
|
Binary file
|
|
Binary file
|
|
@@ -391,7 +391,7 @@ function initializeWebSocket(t2, n2, i2, o2, s2 = false) {
|
|
|
391
391
|
const t3 = new URL(e2);
|
|
392
392
|
return `${t3.hostname}${t3.port ? `:${t3.port}` : ""}`;
|
|
393
393
|
})(t2);
|
|
394
|
-
let a2 = `${"https:" === new URL(t2).protocol ? "wss" : "ws"}://${r2}/ws/notify/?apiKey=${n2}&sessionId=${i2}&sender=JS%2FTS&version=1.
|
|
394
|
+
let a2 = `${"https:" === new URL(t2).protocol ? "wss" : "ws"}://${r2}/ws/notify/?apiKey=${n2}&sessionId=${i2}&sender=JS%2FTS&version=1.11.0`;
|
|
395
395
|
if (o2 && (a2 += `&envValue=${encodeURIComponent(o2)}`), w = s2 ? (function tryCreateWsWorker() {
|
|
396
396
|
if ("undefined" == typeof Worker) return null;
|
|
397
397
|
try {
|
|
@@ -447,11 +447,11 @@ function getFuncSpanHeader() {
|
|
|
447
447
|
return { name: "X-Sf3-FunctionSpanCaptureOverride", value: "1-1-10-10-1-1.0-1-0-0" };
|
|
448
448
|
}
|
|
449
449
|
const j = Object.freeze(Object.defineProperty({ __proto__: null, clearStaleFuncSpanState, disableFunctionSpanTracking, enableFunctionSpanTracking, ensureHrefCache, flushBufferedEvents, getCachedHref, getCachedHrefNoQuery, getFuncSpanHeader, initializeFunctionSpanTrackingFromApi, initializeWebSocket, isFunctionSpanTrackingEnabled, onNavigationChange, restoreFuncSpanState, sendEvent, sendMessage }, Symbol.toStringTag, { value: "Module" }));
|
|
450
|
-
let O = null,
|
|
451
|
-
let
|
|
450
|
+
let O = null, z = null;
|
|
451
|
+
let U = null;
|
|
452
452
|
const H = ["https://api.ipify.org?format=json", "https://api.ip.sb/jsonip", "https://api4.my-ip.io/ip.json"];
|
|
453
453
|
function fetchAndSendIp(e2) {
|
|
454
|
-
|
|
454
|
+
U !== e2 && (U = e2, (async () => {
|
|
455
455
|
for (const e3 of H) try {
|
|
456
456
|
const t2 = new AbortController(), n2 = setTimeout(() => t2.abort(), 5e3), i2 = await fetch(e3, { signal: t2.signal });
|
|
457
457
|
if (clearTimeout(n2), !i2.ok) continue;
|
|
@@ -459,9 +459,9 @@ function fetchAndSendIp(e2) {
|
|
|
459
459
|
if (s2 && "string" == typeof s2 && s2.length <= 45) return void sendMessage({ type: "visitorIp", ip: s2, timestamp: exports.nowTimestamp() });
|
|
460
460
|
} catch {
|
|
461
461
|
}
|
|
462
|
-
|
|
462
|
+
U = null;
|
|
463
463
|
})().catch(() => {
|
|
464
|
-
|
|
464
|
+
U = null;
|
|
465
465
|
}));
|
|
466
466
|
}
|
|
467
467
|
let N = null;
|
|
@@ -711,7 +711,7 @@ async function initializeRecording(e2, n2, i2, o2, s2, r2 = true, a2 = false, l2
|
|
|
711
711
|
}
|
|
712
712
|
const { record: n4 } = await import("@sailfish-rrweb/rrweb-record-only");
|
|
713
713
|
if (Q = n4, await yieldToMain(), l2) {
|
|
714
|
-
const { chunkedSnapshot: i3 } = await Promise.resolve().then(() => require("./chunkSerializer-
|
|
714
|
+
const { chunkedSnapshot: i3 } = await Promise.resolve().then(() => require("./chunkSerializer-CRDpgzTs.js")), o3 = n4.mirror;
|
|
715
715
|
let s3 = true;
|
|
716
716
|
const r3 = [];
|
|
717
717
|
n4({ emit(e3) {
|
|
@@ -2211,7 +2211,7 @@ function setupFetchInterceptor(e2 = [], t2 = { captureStreamingResponseBody: tru
|
|
|
2211
2211
|
})(e3, i3, o3, d2, u2, s2, c2);
|
|
2212
2212
|
} });
|
|
2213
2213
|
}
|
|
2214
|
-
async function startRecording({ apiKey: e2, backendApi: t2 = "https://api-service.sailfishqa.com", domainsToPropagateHeaderTo: i2 = [], domainsToNotPropagateHeaderTo: o2 = [], serviceVersion: s2, serviceIdentifier: r2, gitSha: a2, serviceAdditionalMetadata: l2, enableIpTracking: c2, captureStreamingResponseBody: d2 = true, captureResponseBodyMaxMb: u2 = 10, captureStreamPrefixKb: p2 = 64, captureStreamTimeoutMs: f2 = 1e4, enableFiberTracking: m2 = false, deferRecording: h2, deferRecordingStart: y2, chunkSnapshot: b2, useWsWorker: S2 = true }) {
|
|
2214
|
+
async function startRecording({ apiKey: e2, backendApi: t2 = "https://api-service.sailfishqa.com", domainsToPropagateHeaderTo: i2 = [], domainsToNotPropagateHeaderTo: o2 = [], serviceVersion: s2, serviceIdentifier: r2, gitSha: a2, serviceAdditionalMetadata: l2, enableIpTracking: c2, captureStreamingResponseBody: d2 = true, captureResponseBodyMaxMb: u2 = 10, captureStreamPrefixKb: p2 = 64, captureStreamTimeoutMs: f2 = 1e4, enableFiberTracking: m2 = false, deferRecording: h2, deferRecordingStart: y2, chunkSnapshot: b2, useWsWorker: S2 = true, library: v2 }) {
|
|
2215
2215
|
var _a, _b;
|
|
2216
2216
|
if ((function isHeadlessOrLighthouse() {
|
|
2217
2217
|
try {
|
|
@@ -2224,10 +2224,10 @@ async function startRecording({ apiKey: e2, backendApi: t2 = "https://api-servic
|
|
|
2224
2224
|
return false;
|
|
2225
2225
|
}
|
|
2226
2226
|
})()) return;
|
|
2227
|
-
const
|
|
2228
|
-
if (
|
|
2229
|
-
const
|
|
2230
|
-
sessionStorage.getItem("pageVisitUUID") || (sessionStorage.setItem("pageVisitUUID", uuidv4()), invalidateUrlCache()),
|
|
2227
|
+
const w2 = h2 ?? y2 ?? true, k2 = getOrSetSessionId(), x2 = window.__sailfish_recorder || (window.__sailfish_recorder = {});
|
|
2228
|
+
if (x2.sessionId = k2, x2.apiKey = e2, x2.backendApi = t2, x2.serviceAdditionalMetadata = l2, x2.initialized && x2.sessionId === k2 && x2.ws && 1 === x2.ws.readyState) return void trackDomainChangesOnce();
|
|
2229
|
+
const I2 = { captureStreamingResponseBody: d2, captureResponseBodyMaxMb: u2, captureStreamPrefixKb: p2, captureStreamTimeoutMs: f2 };
|
|
2230
|
+
sessionStorage.getItem("pageVisitUUID") || (sessionStorage.setItem("pageVisitUUID", uuidv4()), invalidateUrlCache()), x2.xhrPatched || (!(function setupXMLHttpRequestInterceptor(e3 = [], t3 = { captureStreamingResponseBody: true, captureResponseBodyMaxMb: 10, captureStreamPrefixKb: 64, captureStreamTimeoutMs: 1e4 }, i3 = []) {
|
|
2231
2231
|
const o3 = XMLHttpRequest.prototype.open, s3 = XMLHttpRequest.prototype.send, r3 = XMLHttpRequest.prototype.setRequestHeader, a3 = getOrSetSessionId(), l3 = createSkipHeadersPropagationChecker(e3, i3);
|
|
2232
2232
|
XMLHttpRequest.prototype.setRequestHeader = function(e4, t4) {
|
|
2233
2233
|
return this._capturedRequestHeaders || (this._capturedRequestHeaders = {}), this._capturedRequestHeaders[e4] = t4, r3.call(this, e4, t4);
|
|
@@ -2292,22 +2292,22 @@ async function startRecording({ apiKey: e2, backendApi: t2 = "https://api-servic
|
|
|
2292
2292
|
emitFinished(false, e5, t4);
|
|
2293
2293
|
}, { once: true }), s3.apply(this, e4);
|
|
2294
2294
|
};
|
|
2295
|
-
})(o2,
|
|
2295
|
+
})(o2, I2, i2), x2.xhrPatched = true), x2.fetchPatched || (setupFetchInterceptor(o2, I2, i2), x2.fetchPatched = true), await yieldToMain(), x2.domEventsInit || (initializeDomContentEvents(k2), x2.domEventsInit = true), await yieldToMain(), x2.consoleInit || (initializeConsolePlugin(Le, k2), x2.consoleInit = true), await yieldToMain(), x2.errorInit || (!(function initializeErrorInterceptor() {
|
|
2296
2296
|
window.addEventListener("error", (e3) => {
|
|
2297
2297
|
captureError(e3.error || e3.message);
|
|
2298
2298
|
}), window.addEventListener("unhandledrejection", (e3) => {
|
|
2299
2299
|
captureError(e3.reason, true);
|
|
2300
2300
|
});
|
|
2301
|
-
})(),
|
|
2301
|
+
})(), x2.errorInit = true), await yieldToMain(), _ensureModuleSideEffects(), (function storeCredentialsAndConnection({ apiKey: e3, backendApi: t3 }) {
|
|
2302
2302
|
g && (sessionStorage.setItem("sailfishApiKey", e3), sessionStorage.setItem("sailfishBackendApi", t3));
|
|
2303
|
-
})({ apiKey: e2, backendApi: t2 }), !isFunctionSpanTrackingEnabled() ||
|
|
2303
|
+
})({ apiKey: e2, backendApi: t2 }), !isFunctionSpanTrackingEnabled() || x2.ws && 1 === x2.ws.readyState || fetchFunctionSpanTrackingEnabled(e2, t2).then((e3) => {
|
|
2304
2304
|
var _a2;
|
|
2305
2305
|
((_a2 = e3.data) == null ? void 0 : _a2.isFunctionSpanTrackingEnabledFromApiKey) ?? false ? we && console.log("[Sailfish] Function span tracking state validated with backend: ACTIVE") : (clearStaleFuncSpanState(), we && console.log("[Sailfish] Cleared stale function span tracking state - backend validation shows tracking is not active"));
|
|
2306
2306
|
}).catch((e3) => {
|
|
2307
2307
|
we && console.warn("[Sailfish] Failed to validate function span tracking status with backend:", e3);
|
|
2308
|
-
}),
|
|
2308
|
+
}), x2.sentDoNotPropagateOnce || (sendDomainsToNotPropagateHeaderTo(e2, [...o2, ...Ie], t2).catch((e3) => console.error("Failed to send domains to not propagate header to:", e3)), x2.sentDoNotPropagateOnce = true), (async function gatherAndCacheDeviceInfo() {
|
|
2309
2309
|
sendMessage({ type: "deviceInfo", data: { deviceInfo: { language: navigator.language, userAgent: navigator.userAgent } } });
|
|
2310
|
-
})(), c2 && fetchAndSendIp(
|
|
2310
|
+
})(), c2 && fetchAndSendIp(k2);
|
|
2311
2311
|
try {
|
|
2312
2312
|
const n2 = a2 ?? (function readGitSha() {
|
|
2313
2313
|
var _a2;
|
|
@@ -2326,7 +2326,7 @@ async function startRecording({ apiKey: e2, backendApi: t2 = "https://api-servic
|
|
|
2326
2326
|
if ("string" == typeof e3 && e3) return e3;
|
|
2327
2327
|
} catch {
|
|
2328
2328
|
}
|
|
2329
|
-
})(), i3 = r2 ?? "", o3 = s2 ?? "", c3 = "JS/TS", d3 = (function getMapUuidFromWindow() {
|
|
2329
|
+
})(), i3 = r2 ?? "", o3 = s2 ?? "", c3 = v2 ?? "JS/TS", d3 = (function getMapUuidFromWindow() {
|
|
2330
2330
|
try {
|
|
2331
2331
|
const e3 = window;
|
|
2332
2332
|
if (e3 && "string" == typeof e3.sfMapUuid && e3.sfMapUuid) return e3.sfMapUuid;
|
|
@@ -2346,15 +2346,15 @@ async function startRecording({ apiKey: e2, backendApi: t2 = "https://api-servic
|
|
|
2346
2346
|
return { framework: i4[0] ?? null, additionalFrameworks: i4.slice(1), serviceRole: "frontend" };
|
|
2347
2347
|
})(), f3 = { ...u3, serviceRole: p3.serviceRole, ...null !== p3.framework && { framework: p3.framework }, ...p3.additionalFrameworks.length > 0 && { additionalFrameworks: p3.additionalFrameworks } };
|
|
2348
2348
|
await yieldToMain();
|
|
2349
|
-
const [g2, h3] = await Promise.all([fetchCaptureSettings(e2, t2), startRecordingSession(e2,
|
|
2350
|
-
if (
|
|
2349
|
+
const [g2, h3] = await Promise.all([fetchCaptureSettings(e2, t2), startRecordingSession(e2, k2, t2, i3, o3, d3, n2, c3, f3)]), y3 = { ...Fe, ...(_a = g2.data) == null ? void 0 : _a.captureSettingsFromApiKey, enableFiberTracking: m2 };
|
|
2350
|
+
if (x2.ws && 1 === x2.ws.readyState) return;
|
|
2351
2351
|
if ((_b = h3.data) == null ? void 0 : _b.startRecordingSession) {
|
|
2352
2352
|
const n3 = (l2 == null ? void 0 : l2.env) || (l2 == null ? void 0 : l2.environment);
|
|
2353
2353
|
await yieldToMain();
|
|
2354
|
-
const i4 = await initializeRecording(y3, t2, e2,
|
|
2355
|
-
|
|
2354
|
+
const i4 = await initializeRecording(y3, t2, e2, k2, n3, w2, S2, b2 ?? false);
|
|
2355
|
+
x2.ws = i4, x2.initialized = true, trackDomainChangesOnce(), x2.sentMapUuidOnce || (!(function sendMapUuidIfAvailable(e3 = "", t3 = "") {
|
|
2356
2356
|
window.sfMapUuid && sendMessage({ type: "mapUuid", data: { mapUuid: window.sfMapUuid, serviceIdentifier: e3, serviceVersion: t3 } });
|
|
2357
|
-
})(r2, s2),
|
|
2357
|
+
})(r2, s2), x2.sentMapUuidOnce = true);
|
|
2358
2358
|
} else console.error("Failed to start recording session:", h3.errors || h3);
|
|
2359
2359
|
} catch (e3) {
|
|
2360
2360
|
console.error("Error starting recording:", e3);
|
|
@@ -2362,7 +2362,7 @@ async function startRecording({ apiKey: e2, backendApi: t2 = "https://api-servic
|
|
|
2362
2362
|
}
|
|
2363
2363
|
exports.DEFAULT_CAPTURE_SETTINGS = Fe, exports.DEFAULT_CONSOLE_RECORDING_SETTINGS = Le, exports.STORAGE_VERSION = 1, exports.addOrUpdateMetadata = function addOrUpdateMetadata(e2) {
|
|
2364
2364
|
const t2 = { type: "addOrUpdateMetadata", metadata: e2 };
|
|
2365
|
-
|
|
2365
|
+
z && JSON.stringify(z) === JSON.stringify(e2) || (z = e2, sendMessage(t2));
|
|
2366
2366
|
}, exports.buildBatches = buildBatches, exports.clearStaleFuncSpanState = clearStaleFuncSpanState, exports.createSkipHeadersPropagationChecker = createSkipHeadersPropagationChecker, exports.createTriageAndIssueFromRecorder = createTriageAndIssueFromRecorder, exports.createTriageFromRecorder = createTriageFromRecorder, exports.disableFunctionSpanTracking = disableFunctionSpanTracking, exports.enableFunctionSpanTracking = enableFunctionSpanTracking, exports.ensureHrefCache = ensureHrefCache, exports.eventSize = eventSize, exports.fetchAndSendIp = fetchAndSendIp, exports.fetchCaptureSettings = fetchCaptureSettings, exports.fetchEngineeringTicketPlatformIntegrations = fetchEngineeringTicketPlatformIntegrations, exports.fetchFunctionSpanTrackingEnabled = fetchFunctionSpanTrackingEnabled, exports.flushBufferedEvents = flushBufferedEvents, exports.getCachedHref = getCachedHref, exports.getCachedHrefNoQuery = getCachedHrefNoQuery, exports.getFuncSpanHeader = getFuncSpanHeader, exports.getOrSetSessionId = getOrSetSessionId, exports.getUrlAndStoredUuids = getUrlAndStoredUuids, exports.identify = function identify(e2, t2 = {}, n2 = false) {
|
|
2367
2367
|
const i2 = { type: "identify", userId: e2, traits: t2 };
|
|
2368
2368
|
O && O.userId === e2 && JSON.stringify(O.traits) === JSON.stringify(t2) || (O = { userId: e2, traits: t2, overwrite: n2 }, sendMessage(i2));
|
|
@@ -2370,15 +2370,19 @@ exports.DEFAULT_CAPTURE_SETTINGS = Fe, exports.DEFAULT_CONSOLE_RECORDING_SETTING
|
|
|
2370
2370
|
if ("undefined" == typeof window) return;
|
|
2371
2371
|
const t2 = window.__sailfish_recorder || (window.__sailfish_recorder = {}), n2 = getOrSetSessionId();
|
|
2372
2372
|
return clearPageVisitDataFromSessionStorage(), t2.initialized && t2.sessionId === n2 && t2.ws && 1 === t2.ws.readyState ? void 0 : (t2.initPromise || (t2.initPromise = (async () => {
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2373
|
+
try {
|
|
2374
|
+
if (t2.hasLoggedInitOnce || (console.log("Initializing Sailfish Recorder (first run) …"), t2.hasLoggedInitOnce = true), await startRecording(e2), !t2.issueReportingInit) {
|
|
2375
|
+
const n3 = e2.backendApi ?? "https://api-service.sailfishqa.com", [{ setupIssueReporting: i2 }, { fetchIntegrationData: o2, getIntegrationData: s2 }] = await Promise.all([Promise.resolve().then(() => ve), Promise.resolve().then(() => le)]);
|
|
2376
|
+
let r2 = null;
|
|
2377
|
+
try {
|
|
2378
|
+
await o2(e2.apiKey, n3), r2 = s2();
|
|
2379
|
+
} catch (e3) {
|
|
2380
|
+
console.warn("[Sailfish] Failed to fetch integration data for issue reporting:", e3);
|
|
2381
|
+
}
|
|
2382
|
+
i2({ apiKey: e2.apiKey, backendApi: n3, getSessionId: () => getOrSetSessionId(), shortcuts: e2.reportIssueShortcuts, customBaseUrl: e2.customBaseUrl, integrationData: r2, showEngTicketFieldsInReportIssueModalDefault: e2.showEngTicketFieldsInReportIssueModalDefault }), t2.issueReportingInit = true;
|
|
2380
2383
|
}
|
|
2381
|
-
|
|
2384
|
+
} catch (e3) {
|
|
2385
|
+
console.warn("[Sailfish] Recorder initialization failed:", e3);
|
|
2382
2386
|
}
|
|
2383
2387
|
})().finally(() => {
|
|
2384
2388
|
delete t2.initPromise;
|
|
Binary file
|
|
Binary file
|