@pydantic/logfire-session-replay 0.1.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 - present Pydantic Services inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # @pydantic/logfire-session-replay
2
+
3
+ Browser session replay recorder for Logfire.
4
+
5
+ This package is experimental while Logfire Platform replay ingest and playback
6
+ are still behind a feature flag. Keep browser replay rollout behind your own
7
+ application flag and expect minor API, ingest, and UI behavior changes before
8
+ general availability.
9
+
10
+ This package records rrweb events, batches them into Logfire replay chunks, and
11
+ uploads gzip-compressed JSON envelopes to a replay upload endpoint. It is
12
+ standalone on purpose: `rrweb` and `fflate` are not dependencies of the core
13
+ `logfire` API package or `@pydantic/logfire-browser`.
14
+
15
+ ## Usage
16
+
17
+ Prefer a backend replay proxy for browser applications. The SDK posts to:
18
+
19
+ ```text
20
+ {replayUrl}/{sessionId}?seq={seq}
21
+ ```
22
+
23
+ Example:
24
+
25
+ ```ts
26
+ import { startSessionReplay } from '@pydantic/logfire-session-replay'
27
+
28
+ const replay = startSessionReplay({
29
+ replayUrl: '/logfire/replay',
30
+ headers: () => ({
31
+ 'X-CSRF-Token': getCsrfToken(),
32
+ }),
33
+
34
+ sessionSampleRate: 0.1,
35
+ onErrorSampleRate: 1,
36
+
37
+ maskAllInputs: true,
38
+ blockSelector: '[data-logfire-block]',
39
+ maskTextSelector: '[data-logfire-mask]',
40
+
41
+ distinctId: currentUser?.id,
42
+ getTraceContext: () => getCurrentTraceContext(),
43
+ })
44
+
45
+ await replay.flush()
46
+ await replay.stop()
47
+ ```
48
+
49
+ Your proxy should forward the compressed request body and these headers to
50
+ Logfire replay ingest:
51
+
52
+ - `Content-Type: application/json`
53
+ - `Content-Encoding: gzip`
54
+
55
+ The decompressed body shape is:
56
+
57
+ ```ts
58
+ {
59
+ version: 1,
60
+ meta: {
61
+ seq,
62
+ firstTimestamp,
63
+ lastTimestamp,
64
+ eventCount,
65
+ clickCount,
66
+ keypressCount,
67
+ errorCount,
68
+ hasFullSnapshot,
69
+ urls,
70
+ traceIds,
71
+ distinctId,
72
+ },
73
+ events,
74
+ }
75
+ ```
76
+
77
+ ## Direct Token Escape Hatch
78
+
79
+ For trusted runtimes or explicitly accepted advanced browser usage, you can
80
+ send directly to Logfire ingest with a write token:
81
+
82
+ ```ts
83
+ startSessionReplay({
84
+ replayUrl: 'https://logfire-us.pydantic.dev/v1/replay',
85
+ token: '<project-write-token>',
86
+ })
87
+ ```
88
+
89
+ Normal browser applications should not expose project write tokens in bundles.
90
+ Use `replayUrl + headers` with a backend proxy when possible.
91
+
92
+ ## Privacy
93
+
94
+ The recorder masks input values by default, disables canvas recording, disables
95
+ font collection, throttles media sampling, and never captures request or response
96
+ bodies. Replay can still capture DOM text, full URLs, console output, fetch/XHR
97
+ metadata, and navigation events depending on configuration.
98
+
99
+ Use `blockSelector` to omit entire DOM subtrees, `maskTextSelector` to mask text
100
+ content before events are recorded, `redactUrlPatterns` to strip query strings
101
+ and fragments from matching network URLs, and `captureConsole`,
102
+ `captureNetwork`, or `captureNavigation` to disable capture classes that are not
103
+ appropriate for your application.
104
+
105
+ ## Sampling
106
+
107
+ The recorder uses a Sentry-style two-rate model:
108
+
109
+ - `sessionSampleRate`: records the full session continuously
110
+ - `onErrorSampleRate`: for sessions not selected by `sessionSampleRate`, keeps
111
+ an in-memory replay buffer and uploads it only if an error occurs
112
+
113
+ `stop()` is awaitable and flushes the final chunk before resolving.
114
+
115
+ ## Correlation
116
+
117
+ Use `getSessionId` to share a session id with another SDK layer. The
118
+ `@pydantic/logfire-browser` integration passes its browser RUM session id through
119
+ this hook when top-level `sessionReplay` is configured.
120
+
121
+ Use `getTraceContext` to stamp active trace ids into the replay stream so the
122
+ replay can be linked to browser traces and errors.
123
+
124
+ ## Browser SDK Integration
125
+
126
+ Most browser applications should enable replay through
127
+ `@pydantic/logfire-browser` instead of calling `startSessionReplay()` directly:
128
+
129
+ ```ts
130
+ import * as logfire from '@pydantic/logfire-browser'
131
+
132
+ logfire.configure({
133
+ traceUrl: '/logfire-proxy/v1/traces',
134
+ serviceName: 'browser-app',
135
+ sessionReplay: {
136
+ load: () => import('@pydantic/logfire-session-replay'),
137
+ replayUrl: '/logfire-proxy/v1/replay',
138
+ },
139
+ })
140
+ ```
141
+
142
+ The browser package owns RUM session identity, span correlation attributes, and
143
+ cleanup ordering. Call `startSessionReplay()` directly only for standalone or
144
+ advanced integrations that do not use `@pydantic/logfire-browser`.
145
+
146
+ ## Local Development Notes
147
+
148
+ Browser privacy extensions or ad blockers may block requests or dynamic imports
149
+ whose URLs contain terms such as `session-replay`. If replay fails to start with
150
+ `ERR_BLOCKED_BY_CLIENT`, test in a clean profile or disable the extension for
151
+ the local app.
152
+
153
+ When a Vite workspace example imports unpublished package output directly from
154
+ `dist`, make sure rrweb resolves to its browser ESM build
155
+ (`rrweb/dist/rrweb.js`). Resolving rrweb to `rrweb.cjs` can fail at runtime
156
+ because that build does not provide the named `record` export used by this
157
+ package.
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("rrweb"),t=require("fflate");const n={DomContentLoaded:0,Load:1,FullSnapshot:2,IncrementalSnapshot:3,Meta:4,Custom:5,Plugin:6},r={Mutation:0,MouseMove:1,MouseInteraction:2,Scroll:3,ViewportResize:4,Input:5},i={MouseUp:0,MouseDown:1,Click:2,ContextMenu:3,DblClick:4},a={Error:`logfire.error`,Trace:`logfire.trace`,Console:`logfire.console`,Network:`logfire.network`,Navigation:`logfire.navigation`},o={sessionSampleRate:1,onErrorSampleRate:1,maskAllInputs:!0,maskTextSelector:``,blockSelector:``,flushIntervalMs:5e3,maxBufferBytes:1e6,sessionIdleTimeoutMs:18e5,maxSessionDurationMs:144e5,distinctId:``,captureConsole:!0,captureNetwork:!0,captureNavigation:!0},s=[`log`,`info`,`warn`,`error`,`debug`],c=1024,l=()=>void 0;function u(e,t={}){let n=new Map,r=!1;for(let r of s){let i=console[r];if(typeof i!=`function`)continue;let o=i;n.set(r,o),console[r]=(...n)=>{_(e,a.Console,{level:r,args:n.slice(0,10).map(v)},t.onError),o.apply(console,n)}}return()=>{if(!r){r=!0;for(let[e,t]of n)console[e]=t}}}function d(e,t){let n=h(t),r=p(e,n),i=m(e,n),a=!1;return()=>{a||(a=!0,r(),i())}}function f(e,t={}){let n=history.pushState,r=history.replaceState,i=!1,o=n=>{_(e,a.Navigation,{url:window.location.href,kind:n},t.onError)};history.pushState=function(...e){n.apply(this,e),o(`push`)},history.replaceState=function(...e){r.apply(this,e),o(`replace`)};let s=()=>{o(`pop`)};return window.addEventListener(`popstate`,s),()=>{i||(i=!0,history.pushState=n,history.replaceState=r,window.removeEventListener(`popstate`,s))}}function p(e,t){let n=window.fetch;if(typeof n!=`function`)return l;let r=!1;return window.fetch=async(r,i)=>{let o=t.now(),s=b(r,i),c=x(r);if(D(c,t.ignoreUrlPatterns))return n(r,i);let l=E(c,t.redactUrlPatterns),u=T(i?.body);try{let c=await n(r,i);return _(e,a.Network,w({method:s,url:l,status:c.status,durationMs:Math.max(0,t.now()-o),reqBytes:u,resBytes:S(c.headers)}),t.onError),c}catch(n){throw _(e,a.Network,w({method:s,url:l,status:0,durationMs:Math.max(0,t.now()-o),failed:!0,reqBytes:u}),t.onError),n}},()=>{r||(r=!0,window.fetch=n)}}function m(e,t){let n=window.XMLHttpRequest.prototype,r=n.open,i=n.send,o=new WeakMap,s=!1;return n.open=function(e,n,i,a,s){let c=n.toString();if(D(c,t.ignoreUrlPatterns)){o.delete(this),r.call(this,e,n,i??!0,a??null,s??null);return}o.set(this,{method:e.toUpperCase(),url:E(c,t.redactUrlPatterns),startedAt:0,reqBytes:0,failed:!1,emitted:!1}),r.call(this,e,n,i??!0,a??null,s??null)},n.send=function(n){let r=o.get(this);if(r===void 0){i.call(this,n);return}r.startedAt=t.now(),r.reqBytes=T(n);let s=()=>{r.failed=!0},c=()=>{r.emitted||(r.emitted=!0,this.removeEventListener(`error`,s),this.removeEventListener(`abort`,s),this.removeEventListener(`timeout`,s),_(e,a.Network,w({method:r.method,url:r.url,status:this.status,durationMs:Math.max(0,t.now()-r.startedAt),failed:r.failed||this.status===0,reqBytes:r.reqBytes,resBytes:C(this)}),t.onError))};this.addEventListener(`error`,s),this.addEventListener(`abort`,s),this.addEventListener(`timeout`,s),this.addEventListener(`loadend`,c,{once:!0});try{i.call(this,n)}catch(n){throw this.removeEventListener(`error`,s),this.removeEventListener(`abort`,s),this.removeEventListener(`timeout`,s),this.removeEventListener(`loadend`,c),_(e,a.Network,w({method:r.method,url:r.url,status:0,durationMs:Math.max(0,t.now()-r.startedAt),failed:!0,reqBytes:r.reqBytes}),t.onError),n}},()=>{s||(s=!0,n.open=r,n.send=i)}}function h(e){return{...e,ignoreUrlPatterns:g(e.ignoreUrlPatterns),redactUrlPatterns:g(e.redactUrlPatterns)}}function g(e){return e.map(e=>{let t=e.flags.replace(/[gy]/gu,``);return t===e.flags?e:new RegExp(e.source,t)})}function _(e,t,n,r){try{e(t,n)}catch(e){try{r?.(e)}catch{}}}function v(e){if(e==null)return String(e);if(typeof e==`string`)return y(e);if(typeof e==`number`||typeof e==`boolean`||typeof e==`bigint`)return String(e);if(e instanceof Error)return y(e.stack??`${e.name}: ${e.message}`);try{return y(JSON.stringify(e))}catch{return Object.prototype.toString.call(e)}}function y(e){return e.length<=c?e:`${e.slice(0,c)}...(+${String(e.length-c)} chars)`}function b(e,t){return(t?.method??(typeof e==`object`&&`method`in e?e.method:`GET`)).toUpperCase()}function x(e){return typeof e==`string`||e instanceof URL?e.toString():e.url}function S(e){let t=e.get(`content-length`);if(t===null)return;let n=Number.parseInt(t,10);return Number.isFinite(n)?n:void 0}function C(e){try{let t=e.getResponseHeader(`content-length`);if(t===null)return;let n=Number.parseInt(t,10);return Number.isFinite(n)?n:void 0}catch{return}}function w(e){return{method:e.method,url:e.url,status:e.status,durationMs:e.durationMs,...e.failed===void 0?{}:{failed:e.failed},...e.reqBytes===void 0?{}:{reqBytes:e.reqBytes},...e.resBytes===void 0?{}:{resBytes:e.resBytes}}}function T(e){if(e==null)return 0;if(typeof e==`string`)return e.length;if(e instanceof ArrayBuffer||ArrayBuffer.isView(e))return e.byteLength;if(e instanceof Blob)return e.size}function E(e,t){if(t.length===0||!t.some(t=>t.test(e)))return e;try{let t=new URL(e,window.location.href);return`${t.origin}${t.pathname}`}catch{let[t=e]=e.split(`?`),[n=t]=t.split(`#`);return n}}function D(e,t){return t.some(t=>t.test(e))}const O=e.record;function ee(e){let t={emit:t=>{e.emit(t)},maskAllInputs:e.maskAllInputs,recordCanvas:!1,collectFonts:!1,sampling:{mousemove:!0,mouseInteraction:!0,scroll:150,media:800,input:`last`}};e.maskTextSelector!==void 0&&e.maskTextSelector.length>0&&(t.maskTextSelector=e.maskTextSelector),e.blockSelector!==void 0&&e.blockSelector.length>0&&(t.blockSelector=e.blockSelector),e.checkoutEveryNms!==void 0&&e.checkoutEveryNms>0&&(t.checkoutEveryNms=e.checkoutEveryNms);let n=O(t);return{stop:()=>{n?.()},addCustomEvent:(e,t)=>{O.addCustomEvent?.(e,t)},takeFullSnapshot:()=>{O.takeFullSnapshot?.(!0)}}}function k(e){return Number.isFinite(e)?Math.min(1,Math.max(0,e)):0}function A(e){let t=e.random??Math.random;return t()<k(e.sessionSampleRate)?`full`:t()<k(e.onErrorSampleRate)?`buffer`:`off`}function j(e=Date.now){let t=e(),n=new Uint8Array(new ArrayBuffer(16));te(n),n[0]=Math.floor(t/2**40)&255,n[1]=Math.floor(t/2**32)&255,n[2]=Math.floor(t/2**24)&255,n[3]=Math.floor(t/2**16)&255,n[4]=Math.floor(t/2**8)&255,n[5]=t&255,n[6]=(n[6]??0)&15|112,n[8]=(n[8]??0)&63|128;let r=``;for(let e of n)r+=e.toString(16).padStart(2,`0`);return`${r.slice(0,8)}-${r.slice(8,12)}-${r.slice(12,16)}-${r.slice(16,20)}-${r.slice(20)}`}function te(e){let t=globalThis.crypto;if(typeof t?.getRandomValues==`function`){t.getRandomValues(e);return}for(let t=0;t<e.length;t++)e[t]=Math.floor(Math.random()*256)}const M=`lf_session_replay`;var N=class{idleTimeoutMs;maxDurationMs;now;storage;memorySession;constructor(e){this.idleTimeoutMs=e.idleTimeoutMs,this.maxDurationMs=e.maxDurationMs,this.now=e.now??Date.now,this.storage=e.storage===void 0?P():e.storage}getSession(){let e=this.now(),t=this.read();return t!==void 0&&!this.isExpired(t,e)?t:this.createSession(e)}touch(){let e=this.now(),t={...this.getSession(),lastActivityAt:e};return this.write(t),t}reset(){return this.createSession(this.now())}isExpired(e,t){return t-e.lastActivityAt>this.idleTimeoutMs||t-e.startedAt>this.maxDurationMs}createSession(e){let t={id:j(this.now),startedAt:e,lastActivityAt:e};return this.write(t),t}read(){if(this.storage!==null)try{let e=this.storage.getItem(M);if(e!==null){let t=JSON.parse(e);if(F(t))return this.memorySession=t,t}}catch{return this.memorySession}return this.memorySession}write(e){if(this.memorySession=e,this.storage!==null)try{this.storage.setItem(M,JSON.stringify(e))}catch{}}};function P(){try{return globalThis.sessionStorage??null}catch{return null}}function F(e){if(typeof e!=`object`||!e)return!1;let t=e;return typeof t.id==`string`&&t.id.length>0&&typeof t.startedAt==`number`&&Number.isFinite(t.startedAt)&&typeof t.lastActivityAt==`number`&&Number.isFinite(t.lastActivityAt)}function I(e){return typeof e==`object`&&e?e:void 0}function L(e){return typeof e==`string`&&e.length>0?e:void 0}function ne(e,t,o){let s=1/0,c=0,l=0,u=0,d=0,f=!1,p=new Set,m=new Set;for(let e of t){e.timestamp<s&&(s=e.timestamp),e.timestamp>c&&(c=e.timestamp);let t=I(e.data);if(e.type===n.FullSnapshot)f=!0;else if(e.type===n.Meta){let e=L(t?.href);e!==void 0&&p.add(e)}else if(e.type===n.IncrementalSnapshot&&t!==void 0){let e=t.source;if(e===r.MouseInteraction){let e=t.type;(e===i.Click||e===i.DblClick)&&l++}else e===r.Input&&u++}else if(e.type===n.Custom&&t!==void 0){let e=L(t.tag),n=I(t.payload);if(e===a.Error)d++;else if(e===a.Trace){let e=L(n?.traceId);e!==void 0&&m.add(e)}else if(e===a.Console)n?.level===`error`&&d++;else if(e===a.Navigation){let e=L(n?.url);e!==void 0&&p.add(e)}}}return{seq:e,firstTimestamp:s===1/0?0:s,lastTimestamp:c,eventCount:t.length,clickCount:l,keypressCount:u,errorCount:d,hasFullSnapshot:f,urls:[...p],traceIds:[...m],...o!==void 0&&o.length>0?{distinctId:o}:{}}}const R=`lf_session_replay_seq`;var re=class{buffer=[];pendingBytes=0;seq=0;timer;mode;flushing;config;storage;sessionId;constructor(e,t,n,r=P()){this.config=e,this.sessionId=t,this.mode=n,this.storage=r,this.seq=this.loadSeq(t)}start(){this.timer!==void 0||this.mode!==`full`||(this.timer=setInterval(()=>{this.flushAndReport()},this.config.flushIntervalMs))}add(e){this.mode===`buffer`&&e.type===n.FullSnapshot&&(this.buffer=[],this.pendingBytes=0),this.buffer.push(e),this.pendingBytes+=H(e),this.mode===`full`&&this.pendingBytes>=this.config.maxBufferBytes&&this.flushAndReport()}async triggerFlush(){return this.mode===`buffer`&&(this.mode=`full`,this.start()),this.flush()}async flush(e={}){if(this.mode===`buffer`||this.buffer.length===0)return;let t=this.buffer;this.buffer=[],this.pendingBytes=0;let n=e.keepalive===!0?U(t):[t],r=this.seq;this.seq+=n.length;let i=this.sessionId;this.saveSeq(i,this.seq);let a=(this.flushing??Promise.resolve()).then(async()=>{for(let t=0;t<n.length;t++){let a=n[t];a!==void 0&&await this.deliver(a,r+t,i,e.keepalive??!1)}});this.flushing=a.catch(()=>void 0),await a}rotate(e){return e===this.sessionId?!1:(this.mode===`buffer`?(this.buffer=[],this.pendingBytes=0):this.flushAndReport(),this.sessionId=e,this.seq=this.loadSeq(e),!0)}async shutdown(e={}){this.timer!==void 0&&(clearInterval(this.timer),this.timer=void 0),await this.flush(e),await this.flushing}getMode(){return this.mode}flushAndReport(){this.flush().catch(e=>{this.config.onError?.(e)})}async deliver(e,n,r,i){let a={version:1,meta:ne(n,e,this.config.getDistinctId?.()??this.config.distinctId),events:e};try{let e=JSON.stringify(a),o=i?(0,t.gzipSync)((0,t.strToU8)(e)):await W(e),s=i&&o.byteLength<=6e4;await this.sendWithRetry(r,n,o,s)}catch(e){this.config.onError?.(e)}}async sendWithRetry(e,t,n,r){let i=r?1:3;for(let a=1;a<=i;a++)try{await this.send(e,t,n,r);return}catch(e){if(e instanceof z&&e.status<500||a>=i)throw e;await V(500*a)}}async send(e,t,n,r){let i=`${this.config.replayUrl.replace(/\/+$/u,``)}/${encodeURIComponent(e)}?seq=${String(t)}`,a=await this.getUploadHeaders(),o=await this.config.fetchImpl(i,{method:`POST`,headers:a,body:n.slice(),keepalive:r});if(!o.ok)throw new z(o.status)}async getUploadHeaders(){let e=this.config.headers===void 0?{}:await this.config.headers(),t=await B(this.config.token);return{...e,...t===void 0||t.length===0?{}:{Authorization:`Bearer ${t}`},"Content-Type":`application/json`,"Content-Encoding":`gzip`}}loadSeq(e){if(this.storage===null)return 0;try{let t=this.storage.getItem(R);if(t===null)return 0;let n=JSON.parse(t);if(typeof n!=`object`||!n)return 0;let r=n;return r.id===e&&typeof r.seq==`number`&&Number.isFinite(r.seq)?r.seq:0}catch{return 0}}saveSeq(e,t){if(this.storage!==null)try{this.storage.setItem(R,JSON.stringify({id:e,seq:t}))}catch{}}},z=class extends Error{status;constructor(e){super(`replay ingest failed: ${String(e)}`),this.status=e,this.name=`ReplayIngestError`}};async function B(e){return typeof e==`function`?e():e}async function V(e){return new Promise(t=>{setTimeout(t,e)})}function H(e){try{return JSON.stringify(e).length}catch{return 0}}function U(e){let t=[],n=[],r=0;for(let i of e){let e=H(i);n.length>0&&r+e>48e3&&(t.push(n),n=[],r=0),n.push(i),r+=e}return n.length>0&&t.push(n),t}async function W(e){return new Promise((n,r)=>{(0,t.gzip)((0,t.strToU8)(e),{level:6},(e,t)=>{if(e!==null){r(e);return}n(t)})})}const G={mode:`off`,recording:!1,getSessionId:()=>``,flush:async()=>Promise.resolve(),stop:async()=>Promise.resolve()},K=`lf_session_replay_mode`;function q(e){if(typeof window>`u`||typeof document>`u`)return G;let t=J(e),n=new N({idleTimeoutMs:t.sessionIdleTimeoutMs,maxDurationMs:t.maxSessionDurationMs,now:t.now}),r=e=>{let r=t.getSessionId?.();return r!==void 0&&r.length>0?r:e?n.touch().id:n.getSession().id},i=r(!1),o=P(),s=Y(t,i,o);if(s===`off`)return G;let c=new re(t,i,s),l={},p=ee({emit:e=>{let t=r(!0);c.rotate(t)&&l.current?.takeFullSnapshot(),c.add(e)},maskAllInputs:t.maskAllInputs,maskTextSelector:t.maskTextSelector,blockSelector:t.blockSelector,checkoutEveryNms:s===`buffer`?12e4:0});l.current=p;let m,h=t.getTraceContext===void 0?void 0:setInterval(()=>{let e=t.getTraceContext?.(),n=e?.traceId;n!==void 0&&n.length>0&&n!==m&&(m=n,p.addCustomEvent(a.Trace,{traceId:n,spanId:e?.spanId}))},1e3),g=e=>{p.addCustomEvent(a.Error,e),c.getMode()===`buffer`&&(X(o,r(!1),`full`),Q(c.triggerFlush(),t.onError))},_=e=>{g($(e.message,e.filename,oe(e.error)))},v=e=>{let t=e.reason;g($(t?.message??String(e.reason??`unhandledrejection`),void 0,t?.stack))},y=()=>{document.visibilityState===`hidden`&&Q(c.flush({keepalive:!0}),t.onError)};window.addEventListener(`error`,_,!0),window.addEventListener(`unhandledrejection`,v,!0),document.addEventListener(`visibilitychange`,y),window.addEventListener(`pagehide`,y);let b=(e,t)=>{p.addCustomEvent(e,t)},x=t.captureConsole?u(b,{onError:t.onError}):Z,S=t.captureNetwork?d(b,{ignoreUrlPatterns:t.ignoreUrlPatterns,now:t.now,onError:t.onError,redactUrlPatterns:t.redactUrlPatterns}):Z,C=t.captureNavigation?f(b,{onError:t.onError}):Z;c.start();let w;return{get mode(){return c.getMode()},recording:!0,getSessionId:()=>r(!1),flush:async()=>c.flush(),stop:async()=>(w??=(async()=>{h!==void 0&&clearInterval(h),window.removeEventListener(`error`,_,!0),window.removeEventListener(`unhandledrejection`,v,!0),document.removeEventListener(`visibilitychange`,y),window.removeEventListener(`pagehide`,y),x(),S(),C(),p.stop(),await c.shutdown({keepalive:!1})})(),w)}}function J(e){if(e.replayUrl.length===0)throw Error("logfire session replay: `replayUrl` is required");let t=e.fetchImpl??(typeof fetch==`function`?fetch.bind(globalThis):void 0);if(t===void 0)throw Error("logfire session replay: no `fetch` available; pass `fetchImpl`");return{replayUrl:e.replayUrl,headers:e.headers,token:e.token,getSessionId:e.getSessionId,sessionSampleRate:e.sessionSampleRate??o.sessionSampleRate,onErrorSampleRate:e.onErrorSampleRate??o.onErrorSampleRate,maskAllInputs:e.maskAllInputs??o.maskAllInputs,maskTextSelector:e.maskTextSelector??o.maskTextSelector,blockSelector:e.blockSelector??o.blockSelector,flushIntervalMs:e.flushIntervalMs??o.flushIntervalMs,maxBufferBytes:e.maxBufferBytes??o.maxBufferBytes,sessionIdleTimeoutMs:e.sessionIdleTimeoutMs??o.sessionIdleTimeoutMs,maxSessionDurationMs:e.maxSessionDurationMs??o.maxSessionDurationMs,distinctId:e.distinctId??o.distinctId,getDistinctId:e.getDistinctId,getTraceContext:e.getTraceContext,captureConsole:e.captureConsole??o.captureConsole,captureNetwork:e.captureNetwork??o.captureNetwork,captureNavigation:e.captureNavigation??o.captureNavigation,ignoreUrlPatterns:e.ignoreUrlPatterns??[],redactUrlPatterns:e.redactUrlPatterns??[],onError:e.onError,fetchImpl:t,now:e.now??Date.now,random:e.random??Math.random}}function Y(e,t,n){let r=ie(n,t);if(r!==void 0)return r;let i=A({sessionSampleRate:e.sessionSampleRate,onErrorSampleRate:e.onErrorSampleRate,random:e.random});return X(n,t,i),i}function ie(e,t){if(e!==null)try{let n=e.getItem(K);if(n===null)return;let r=JSON.parse(n);if(typeof r!=`object`||!r)return;let i=r;return i.id!==t||!ae(i.mode)?void 0:i.mode}catch{return}}function X(e,t,n){if(e!==null)try{e.setItem(K,JSON.stringify({id:t,mode:n}))}catch{}}function ae(e){return e===`full`||e===`buffer`||e===`off`}function oe(e){let t=typeof e==`object`&&e&&`stack`in e?e.stack:void 0;return typeof t==`string`?t:void 0}function Z(){}function Q(e,t){e.catch(e=>{t?.(e)})}function $(e,t,n){return{message:e,...t===void 0||t.length===0?{}:{source:t},...n===void 0?{}:{stack:n}}}exports.CHUNK_ENVELOPE_VERSION=1,exports.CustomTag=a,exports.EventType=n,exports.IncrementalSource=r,exports.MouseInteractions=i,exports.startSessionReplay=q;
@@ -0,0 +1,143 @@
1
+ //#region src/types.d.ts
2
+ interface RrwebEvent {
3
+ type: number;
4
+ data: unknown;
5
+ timestamp: number;
6
+ }
7
+ declare const EventType: {
8
+ readonly DomContentLoaded: 0;
9
+ readonly Load: 1;
10
+ readonly FullSnapshot: 2;
11
+ readonly IncrementalSnapshot: 3;
12
+ readonly Meta: 4;
13
+ readonly Custom: 5;
14
+ readonly Plugin: 6;
15
+ };
16
+ declare const IncrementalSource: {
17
+ readonly Mutation: 0;
18
+ readonly MouseMove: 1;
19
+ readonly MouseInteraction: 2;
20
+ readonly Scroll: 3;
21
+ readonly ViewportResize: 4;
22
+ readonly Input: 5;
23
+ };
24
+ declare const MouseInteractions: {
25
+ readonly MouseUp: 0;
26
+ readonly MouseDown: 1;
27
+ readonly Click: 2;
28
+ readonly ContextMenu: 3;
29
+ readonly DblClick: 4;
30
+ };
31
+ declare const CustomTag: {
32
+ readonly Error: "logfire.error";
33
+ readonly Trace: "logfire.trace";
34
+ readonly Console: "logfire.console";
35
+ readonly Network: "logfire.network";
36
+ readonly Navigation: "logfire.navigation";
37
+ };
38
+ declare const CHUNK_ENVELOPE_VERSION: 1;
39
+ type MaybePromise<T> = T | Promise<T>;
40
+ type ConsoleLevel = "log" | "info" | "warn" | "error" | "debug";
41
+ type SamplingMode = "full" | "buffer" | "off";
42
+ interface ConsolePayload {
43
+ level: ConsoleLevel;
44
+ args: string[];
45
+ source?: string;
46
+ }
47
+ interface NetworkPayload {
48
+ method: string;
49
+ url: string;
50
+ status: number;
51
+ durationMs: number;
52
+ failed?: boolean;
53
+ reqBytes?: number;
54
+ resBytes?: number;
55
+ }
56
+ interface NavigationPayload {
57
+ url: string;
58
+ kind: "load" | "push" | "replace" | "pop";
59
+ }
60
+ interface ChunkMeta {
61
+ seq: number;
62
+ firstTimestamp: number;
63
+ lastTimestamp: number;
64
+ eventCount: number;
65
+ clickCount: number;
66
+ keypressCount: number;
67
+ errorCount: number;
68
+ hasFullSnapshot: boolean;
69
+ urls: string[];
70
+ traceIds: string[];
71
+ distinctId?: string;
72
+ }
73
+ interface ChunkEnvelope {
74
+ version: typeof CHUNK_ENVELOPE_VERSION;
75
+ meta: ChunkMeta;
76
+ events: RrwebEvent[];
77
+ }
78
+ /**
79
+ * Experimental session replay recorder configuration.
80
+ *
81
+ * Logfire Platform replay ingest and playback are still feature-flagged, so
82
+ * keep browser replay rollout behind an application flag.
83
+ */
84
+ interface SessionReplayConfig {
85
+ /**
86
+ * Replay upload endpoint. For normal browser applications this should be a
87
+ * backend proxy endpoint. With the direct-token escape hatch, this may point
88
+ * at Logfire ingest. The SDK posts to `${replayUrl}/${sessionId}?seq=${seq}`.
89
+ */
90
+ replayUrl: string;
91
+ /**
92
+ * Headers added to each replay upload. Use this for CSRF/session auth to the
93
+ * caller's backend proxy.
94
+ */
95
+ headers?: () => MaybePromise<Record<string, string>>;
96
+ /**
97
+ * Advanced escape hatch for direct Logfire ingest. Prefer `headers` with a
98
+ * backend proxy for normal browser applications. When provided, the SDK adds
99
+ * `Authorization: Bearer ${token}` to replay uploads.
100
+ */
101
+ token?: string | (() => MaybePromise<string>);
102
+ /**
103
+ * Optional external session id source. The browser SDK integration should
104
+ * pass its RUM session id here.
105
+ */
106
+ getSessionId?: () => string | undefined;
107
+ sessionSampleRate?: number;
108
+ onErrorSampleRate?: number;
109
+ maskAllInputs?: boolean;
110
+ maskTextSelector?: string;
111
+ blockSelector?: string;
112
+ flushIntervalMs?: number;
113
+ maxBufferBytes?: number;
114
+ sessionIdleTimeoutMs?: number;
115
+ maxSessionDurationMs?: number;
116
+ distinctId?: string;
117
+ getDistinctId?: () => string | undefined;
118
+ getTraceContext?: () => {
119
+ traceId?: string;
120
+ spanId?: string;
121
+ } | undefined;
122
+ captureConsole?: boolean;
123
+ captureNetwork?: boolean;
124
+ captureNavigation?: boolean;
125
+ ignoreUrlPatterns?: RegExp[];
126
+ redactUrlPatterns?: RegExp[];
127
+ onError?: (error: unknown) => void;
128
+ fetchImpl?: typeof fetch;
129
+ now?: () => number;
130
+ random?: () => number;
131
+ }
132
+ //#endregion
133
+ //#region src/index.d.ts
134
+ interface SessionReplay {
135
+ readonly recording: boolean;
136
+ readonly mode: "full" | "buffer" | "off";
137
+ getSessionId(): string;
138
+ flush(): Promise<void>;
139
+ stop(): Promise<void>;
140
+ }
141
+ declare function startSessionReplay(config: SessionReplayConfig): SessionReplay;
142
+ //#endregion
143
+ export { CHUNK_ENVELOPE_VERSION, type ChunkEnvelope, type ChunkMeta, type ConsolePayload, CustomTag, EventType, IncrementalSource, MouseInteractions, type NavigationPayload, type NetworkPayload, type RrwebEvent, type SamplingMode, SessionReplay, type SessionReplayConfig, startSessionReplay };
@@ -0,0 +1,143 @@
1
+ //#region src/types.d.ts
2
+ interface RrwebEvent {
3
+ type: number;
4
+ data: unknown;
5
+ timestamp: number;
6
+ }
7
+ declare const EventType: {
8
+ readonly DomContentLoaded: 0;
9
+ readonly Load: 1;
10
+ readonly FullSnapshot: 2;
11
+ readonly IncrementalSnapshot: 3;
12
+ readonly Meta: 4;
13
+ readonly Custom: 5;
14
+ readonly Plugin: 6;
15
+ };
16
+ declare const IncrementalSource: {
17
+ readonly Mutation: 0;
18
+ readonly MouseMove: 1;
19
+ readonly MouseInteraction: 2;
20
+ readonly Scroll: 3;
21
+ readonly ViewportResize: 4;
22
+ readonly Input: 5;
23
+ };
24
+ declare const MouseInteractions: {
25
+ readonly MouseUp: 0;
26
+ readonly MouseDown: 1;
27
+ readonly Click: 2;
28
+ readonly ContextMenu: 3;
29
+ readonly DblClick: 4;
30
+ };
31
+ declare const CustomTag: {
32
+ readonly Error: "logfire.error";
33
+ readonly Trace: "logfire.trace";
34
+ readonly Console: "logfire.console";
35
+ readonly Network: "logfire.network";
36
+ readonly Navigation: "logfire.navigation";
37
+ };
38
+ declare const CHUNK_ENVELOPE_VERSION: 1;
39
+ type MaybePromise<T> = T | Promise<T>;
40
+ type ConsoleLevel = "log" | "info" | "warn" | "error" | "debug";
41
+ type SamplingMode = "full" | "buffer" | "off";
42
+ interface ConsolePayload {
43
+ level: ConsoleLevel;
44
+ args: string[];
45
+ source?: string;
46
+ }
47
+ interface NetworkPayload {
48
+ method: string;
49
+ url: string;
50
+ status: number;
51
+ durationMs: number;
52
+ failed?: boolean;
53
+ reqBytes?: number;
54
+ resBytes?: number;
55
+ }
56
+ interface NavigationPayload {
57
+ url: string;
58
+ kind: "load" | "push" | "replace" | "pop";
59
+ }
60
+ interface ChunkMeta {
61
+ seq: number;
62
+ firstTimestamp: number;
63
+ lastTimestamp: number;
64
+ eventCount: number;
65
+ clickCount: number;
66
+ keypressCount: number;
67
+ errorCount: number;
68
+ hasFullSnapshot: boolean;
69
+ urls: string[];
70
+ traceIds: string[];
71
+ distinctId?: string;
72
+ }
73
+ interface ChunkEnvelope {
74
+ version: typeof CHUNK_ENVELOPE_VERSION;
75
+ meta: ChunkMeta;
76
+ events: RrwebEvent[];
77
+ }
78
+ /**
79
+ * Experimental session replay recorder configuration.
80
+ *
81
+ * Logfire Platform replay ingest and playback are still feature-flagged, so
82
+ * keep browser replay rollout behind an application flag.
83
+ */
84
+ interface SessionReplayConfig {
85
+ /**
86
+ * Replay upload endpoint. For normal browser applications this should be a
87
+ * backend proxy endpoint. With the direct-token escape hatch, this may point
88
+ * at Logfire ingest. The SDK posts to `${replayUrl}/${sessionId}?seq=${seq}`.
89
+ */
90
+ replayUrl: string;
91
+ /**
92
+ * Headers added to each replay upload. Use this for CSRF/session auth to the
93
+ * caller's backend proxy.
94
+ */
95
+ headers?: () => MaybePromise<Record<string, string>>;
96
+ /**
97
+ * Advanced escape hatch for direct Logfire ingest. Prefer `headers` with a
98
+ * backend proxy for normal browser applications. When provided, the SDK adds
99
+ * `Authorization: Bearer ${token}` to replay uploads.
100
+ */
101
+ token?: string | (() => MaybePromise<string>);
102
+ /**
103
+ * Optional external session id source. The browser SDK integration should
104
+ * pass its RUM session id here.
105
+ */
106
+ getSessionId?: () => string | undefined;
107
+ sessionSampleRate?: number;
108
+ onErrorSampleRate?: number;
109
+ maskAllInputs?: boolean;
110
+ maskTextSelector?: string;
111
+ blockSelector?: string;
112
+ flushIntervalMs?: number;
113
+ maxBufferBytes?: number;
114
+ sessionIdleTimeoutMs?: number;
115
+ maxSessionDurationMs?: number;
116
+ distinctId?: string;
117
+ getDistinctId?: () => string | undefined;
118
+ getTraceContext?: () => {
119
+ traceId?: string;
120
+ spanId?: string;
121
+ } | undefined;
122
+ captureConsole?: boolean;
123
+ captureNetwork?: boolean;
124
+ captureNavigation?: boolean;
125
+ ignoreUrlPatterns?: RegExp[];
126
+ redactUrlPatterns?: RegExp[];
127
+ onError?: (error: unknown) => void;
128
+ fetchImpl?: typeof fetch;
129
+ now?: () => number;
130
+ random?: () => number;
131
+ }
132
+ //#endregion
133
+ //#region src/index.d.ts
134
+ interface SessionReplay {
135
+ readonly recording: boolean;
136
+ readonly mode: "full" | "buffer" | "off";
137
+ getSessionId(): string;
138
+ flush(): Promise<void>;
139
+ stop(): Promise<void>;
140
+ }
141
+ declare function startSessionReplay(config: SessionReplayConfig): SessionReplay;
142
+ //#endregion
143
+ export { CHUNK_ENVELOPE_VERSION, type ChunkEnvelope, type ChunkMeta, type ConsolePayload, CustomTag, EventType, IncrementalSource, MouseInteractions, type NavigationPayload, type NetworkPayload, type RrwebEvent, type SamplingMode, SessionReplay, type SessionReplayConfig, startSessionReplay };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{record as e}from"rrweb";import{gzip as t,gzipSync as n,strToU8 as r}from"fflate";const i={DomContentLoaded:0,Load:1,FullSnapshot:2,IncrementalSnapshot:3,Meta:4,Custom:5,Plugin:6},a={Mutation:0,MouseMove:1,MouseInteraction:2,Scroll:3,ViewportResize:4,Input:5},o={MouseUp:0,MouseDown:1,Click:2,ContextMenu:3,DblClick:4},s={Error:`logfire.error`,Trace:`logfire.trace`,Console:`logfire.console`,Network:`logfire.network`,Navigation:`logfire.navigation`},c=1,l={sessionSampleRate:1,onErrorSampleRate:1,maskAllInputs:!0,maskTextSelector:``,blockSelector:``,flushIntervalMs:5e3,maxBufferBytes:1e6,sessionIdleTimeoutMs:18e5,maxSessionDurationMs:144e5,distinctId:``,captureConsole:!0,captureNetwork:!0,captureNavigation:!0},u=[`log`,`info`,`warn`,`error`,`debug`],d=1024,f=()=>void 0;function p(e,t={}){let n=new Map,r=!1;for(let r of u){let i=console[r];if(typeof i!=`function`)continue;let a=i;n.set(r,a),console[r]=(...n)=>{b(e,s.Console,{level:r,args:n.slice(0,10).map(x)},t.onError),a.apply(console,n)}}return()=>{if(!r){r=!0;for(let[e,t]of n)console[e]=t}}}function m(e,t){let n=v(t),r=g(e,n),i=_(e,n),a=!1;return()=>{a||(a=!0,r(),i())}}function h(e,t={}){let n=history.pushState,r=history.replaceState,i=!1,a=n=>{b(e,s.Navigation,{url:window.location.href,kind:n},t.onError)};history.pushState=function(...e){n.apply(this,e),a(`push`)},history.replaceState=function(...e){r.apply(this,e),a(`replace`)};let o=()=>{a(`pop`)};return window.addEventListener(`popstate`,o),()=>{i||(i=!0,history.pushState=n,history.replaceState=r,window.removeEventListener(`popstate`,o))}}function g(e,t){let n=window.fetch;if(typeof n!=`function`)return f;let r=!1;return window.fetch=async(r,i)=>{let a=t.now(),o=C(r,i),c=w(r);if(O(c,t.ignoreUrlPatterns))return n(r,i);let l=D(c,t.redactUrlPatterns),u=E(i?.body);try{let c=await n(r,i);return b(e,s.Network,T({method:o,url:l,status:c.status,durationMs:Math.max(0,t.now()-a),reqBytes:u,resBytes:ee(c.headers)}),t.onError),c}catch(n){throw b(e,s.Network,T({method:o,url:l,status:0,durationMs:Math.max(0,t.now()-a),failed:!0,reqBytes:u}),t.onError),n}},()=>{r||(r=!0,window.fetch=n)}}function _(e,t){let n=window.XMLHttpRequest.prototype,r=n.open,i=n.send,a=new WeakMap,o=!1;return n.open=function(e,n,i,o,s){let c=n.toString();if(O(c,t.ignoreUrlPatterns)){a.delete(this),r.call(this,e,n,i??!0,o??null,s??null);return}a.set(this,{method:e.toUpperCase(),url:D(c,t.redactUrlPatterns),startedAt:0,reqBytes:0,failed:!1,emitted:!1}),r.call(this,e,n,i??!0,o??null,s??null)},n.send=function(n){let r=a.get(this);if(r===void 0){i.call(this,n);return}r.startedAt=t.now(),r.reqBytes=E(n);let o=()=>{r.failed=!0},c=()=>{r.emitted||(r.emitted=!0,this.removeEventListener(`error`,o),this.removeEventListener(`abort`,o),this.removeEventListener(`timeout`,o),b(e,s.Network,T({method:r.method,url:r.url,status:this.status,durationMs:Math.max(0,t.now()-r.startedAt),failed:r.failed||this.status===0,reqBytes:r.reqBytes,resBytes:te(this)}),t.onError))};this.addEventListener(`error`,o),this.addEventListener(`abort`,o),this.addEventListener(`timeout`,o),this.addEventListener(`loadend`,c,{once:!0});try{i.call(this,n)}catch(n){throw this.removeEventListener(`error`,o),this.removeEventListener(`abort`,o),this.removeEventListener(`timeout`,o),this.removeEventListener(`loadend`,c),b(e,s.Network,T({method:r.method,url:r.url,status:0,durationMs:Math.max(0,t.now()-r.startedAt),failed:!0,reqBytes:r.reqBytes}),t.onError),n}},()=>{o||(o=!0,n.open=r,n.send=i)}}function v(e){return{...e,ignoreUrlPatterns:y(e.ignoreUrlPatterns),redactUrlPatterns:y(e.redactUrlPatterns)}}function y(e){return e.map(e=>{let t=e.flags.replace(/[gy]/gu,``);return t===e.flags?e:new RegExp(e.source,t)})}function b(e,t,n,r){try{e(t,n)}catch(e){try{r?.(e)}catch{}}}function x(e){if(e==null)return String(e);if(typeof e==`string`)return S(e);if(typeof e==`number`||typeof e==`boolean`||typeof e==`bigint`)return String(e);if(e instanceof Error)return S(e.stack??`${e.name}: ${e.message}`);try{return S(JSON.stringify(e))}catch{return Object.prototype.toString.call(e)}}function S(e){return e.length<=d?e:`${e.slice(0,d)}...(+${String(e.length-d)} chars)`}function C(e,t){return(t?.method??(typeof e==`object`&&`method`in e?e.method:`GET`)).toUpperCase()}function w(e){return typeof e==`string`||e instanceof URL?e.toString():e.url}function ee(e){let t=e.get(`content-length`);if(t===null)return;let n=Number.parseInt(t,10);return Number.isFinite(n)?n:void 0}function te(e){try{let t=e.getResponseHeader(`content-length`);if(t===null)return;let n=Number.parseInt(t,10);return Number.isFinite(n)?n:void 0}catch{return}}function T(e){return{method:e.method,url:e.url,status:e.status,durationMs:e.durationMs,...e.failed===void 0?{}:{failed:e.failed},...e.reqBytes===void 0?{}:{reqBytes:e.reqBytes},...e.resBytes===void 0?{}:{resBytes:e.resBytes}}}function E(e){if(e==null)return 0;if(typeof e==`string`)return e.length;if(e instanceof ArrayBuffer||ArrayBuffer.isView(e))return e.byteLength;if(e instanceof Blob)return e.size}function D(e,t){if(t.length===0||!t.some(t=>t.test(e)))return e;try{let t=new URL(e,window.location.href);return`${t.origin}${t.pathname}`}catch{let[t=e]=e.split(`?`),[n=t]=t.split(`#`);return n}}function O(e,t){return t.some(t=>t.test(e))}const k=e;function ne(e){let t={emit:t=>{e.emit(t)},maskAllInputs:e.maskAllInputs,recordCanvas:!1,collectFonts:!1,sampling:{mousemove:!0,mouseInteraction:!0,scroll:150,media:800,input:`last`}};e.maskTextSelector!==void 0&&e.maskTextSelector.length>0&&(t.maskTextSelector=e.maskTextSelector),e.blockSelector!==void 0&&e.blockSelector.length>0&&(t.blockSelector=e.blockSelector),e.checkoutEveryNms!==void 0&&e.checkoutEveryNms>0&&(t.checkoutEveryNms=e.checkoutEveryNms);let n=k(t);return{stop:()=>{n?.()},addCustomEvent:(e,t)=>{k.addCustomEvent?.(e,t)},takeFullSnapshot:()=>{k.takeFullSnapshot?.(!0)}}}function A(e){return Number.isFinite(e)?Math.min(1,Math.max(0,e)):0}function j(e){let t=e.random??Math.random;return t()<A(e.sessionSampleRate)?`full`:t()<A(e.onErrorSampleRate)?`buffer`:`off`}function M(e=Date.now){let t=e(),n=new Uint8Array(new ArrayBuffer(16));N(n),n[0]=Math.floor(t/2**40)&255,n[1]=Math.floor(t/2**32)&255,n[2]=Math.floor(t/2**24)&255,n[3]=Math.floor(t/2**16)&255,n[4]=Math.floor(t/2**8)&255,n[5]=t&255,n[6]=(n[6]??0)&15|112,n[8]=(n[8]??0)&63|128;let r=``;for(let e of n)r+=e.toString(16).padStart(2,`0`);return`${r.slice(0,8)}-${r.slice(8,12)}-${r.slice(12,16)}-${r.slice(16,20)}-${r.slice(20)}`}function N(e){let t=globalThis.crypto;if(typeof t?.getRandomValues==`function`){t.getRandomValues(e);return}for(let t=0;t<e.length;t++)e[t]=Math.floor(Math.random()*256)}const P=`lf_session_replay`;var F=class{idleTimeoutMs;maxDurationMs;now;storage;memorySession;constructor(e){this.idleTimeoutMs=e.idleTimeoutMs,this.maxDurationMs=e.maxDurationMs,this.now=e.now??Date.now,this.storage=e.storage===void 0?I():e.storage}getSession(){let e=this.now(),t=this.read();return t!==void 0&&!this.isExpired(t,e)?t:this.createSession(e)}touch(){let e=this.now(),t={...this.getSession(),lastActivityAt:e};return this.write(t),t}reset(){return this.createSession(this.now())}isExpired(e,t){return t-e.lastActivityAt>this.idleTimeoutMs||t-e.startedAt>this.maxDurationMs}createSession(e){let t={id:M(this.now),startedAt:e,lastActivityAt:e};return this.write(t),t}read(){if(this.storage!==null)try{let e=this.storage.getItem(P);if(e!==null){let t=JSON.parse(e);if(re(t))return this.memorySession=t,t}}catch{return this.memorySession}return this.memorySession}write(e){if(this.memorySession=e,this.storage!==null)try{this.storage.setItem(P,JSON.stringify(e))}catch{}}};function I(){try{return globalThis.sessionStorage??null}catch{return null}}function re(e){if(typeof e!=`object`||!e)return!1;let t=e;return typeof t.id==`string`&&t.id.length>0&&typeof t.startedAt==`number`&&Number.isFinite(t.startedAt)&&typeof t.lastActivityAt==`number`&&Number.isFinite(t.lastActivityAt)}function L(e){return typeof e==`object`&&e?e:void 0}function R(e){return typeof e==`string`&&e.length>0?e:void 0}function ie(e,t,n){let r=1/0,c=0,l=0,u=0,d=0,f=!1,p=new Set,m=new Set;for(let e of t){e.timestamp<r&&(r=e.timestamp),e.timestamp>c&&(c=e.timestamp);let t=L(e.data);if(e.type===i.FullSnapshot)f=!0;else if(e.type===i.Meta){let e=R(t?.href);e!==void 0&&p.add(e)}else if(e.type===i.IncrementalSnapshot&&t!==void 0){let e=t.source;if(e===a.MouseInteraction){let e=t.type;(e===o.Click||e===o.DblClick)&&l++}else e===a.Input&&u++}else if(e.type===i.Custom&&t!==void 0){let e=R(t.tag),n=L(t.payload);if(e===s.Error)d++;else if(e===s.Trace){let e=R(n?.traceId);e!==void 0&&m.add(e)}else if(e===s.Console)n?.level===`error`&&d++;else if(e===s.Navigation){let e=R(n?.url);e!==void 0&&p.add(e)}}}return{seq:e,firstTimestamp:r===1/0?0:r,lastTimestamp:c,eventCount:t.length,clickCount:l,keypressCount:u,errorCount:d,hasFullSnapshot:f,urls:[...p],traceIds:[...m],...n!==void 0&&n.length>0?{distinctId:n}:{}}}const z=`lf_session_replay_seq`;var B=class{buffer=[];pendingBytes=0;seq=0;timer;mode;flushing;config;storage;sessionId;constructor(e,t,n,r=I()){this.config=e,this.sessionId=t,this.mode=n,this.storage=r,this.seq=this.loadSeq(t)}start(){this.timer!==void 0||this.mode!==`full`||(this.timer=setInterval(()=>{this.flushAndReport()},this.config.flushIntervalMs))}add(e){this.mode===`buffer`&&e.type===i.FullSnapshot&&(this.buffer=[],this.pendingBytes=0),this.buffer.push(e),this.pendingBytes+=W(e),this.mode===`full`&&this.pendingBytes>=this.config.maxBufferBytes&&this.flushAndReport()}async triggerFlush(){return this.mode===`buffer`&&(this.mode=`full`,this.start()),this.flush()}async flush(e={}){if(this.mode===`buffer`||this.buffer.length===0)return;let t=this.buffer;this.buffer=[],this.pendingBytes=0;let n=e.keepalive===!0?G(t):[t],r=this.seq;this.seq+=n.length;let i=this.sessionId;this.saveSeq(i,this.seq);let a=(this.flushing??Promise.resolve()).then(async()=>{for(let t=0;t<n.length;t++){let a=n[t];a!==void 0&&await this.deliver(a,r+t,i,e.keepalive??!1)}});this.flushing=a.catch(()=>void 0),await a}rotate(e){return e===this.sessionId?!1:(this.mode===`buffer`?(this.buffer=[],this.pendingBytes=0):this.flushAndReport(),this.sessionId=e,this.seq=this.loadSeq(e),!0)}async shutdown(e={}){this.timer!==void 0&&(clearInterval(this.timer),this.timer=void 0),await this.flush(e),await this.flushing}getMode(){return this.mode}flushAndReport(){this.flush().catch(e=>{this.config.onError?.(e)})}async deliver(e,t,i,a){let o={version:1,meta:ie(t,e,this.config.getDistinctId?.()??this.config.distinctId),events:e};try{let e=JSON.stringify(o),s=a?n(r(e)):await K(e),c=a&&s.byteLength<=6e4;await this.sendWithRetry(i,t,s,c)}catch(e){this.config.onError?.(e)}}async sendWithRetry(e,t,n,r){let i=r?1:3;for(let a=1;a<=i;a++)try{await this.send(e,t,n,r);return}catch(e){if(e instanceof V&&e.status<500||a>=i)throw e;await U(500*a)}}async send(e,t,n,r){let i=`${this.config.replayUrl.replace(/\/+$/u,``)}/${encodeURIComponent(e)}?seq=${String(t)}`,a=await this.getUploadHeaders(),o=await this.config.fetchImpl(i,{method:`POST`,headers:a,body:n.slice(),keepalive:r});if(!o.ok)throw new V(o.status)}async getUploadHeaders(){let e=this.config.headers===void 0?{}:await this.config.headers(),t=await H(this.config.token);return{...e,...t===void 0||t.length===0?{}:{Authorization:`Bearer ${t}`},"Content-Type":`application/json`,"Content-Encoding":`gzip`}}loadSeq(e){if(this.storage===null)return 0;try{let t=this.storage.getItem(z);if(t===null)return 0;let n=JSON.parse(t);if(typeof n!=`object`||!n)return 0;let r=n;return r.id===e&&typeof r.seq==`number`&&Number.isFinite(r.seq)?r.seq:0}catch{return 0}}saveSeq(e,t){if(this.storage!==null)try{this.storage.setItem(z,JSON.stringify({id:e,seq:t}))}catch{}}},V=class extends Error{status;constructor(e){super(`replay ingest failed: ${String(e)}`),this.status=e,this.name=`ReplayIngestError`}};async function H(e){return typeof e==`function`?e():e}async function U(e){return new Promise(t=>{setTimeout(t,e)})}function W(e){try{return JSON.stringify(e).length}catch{return 0}}function G(e){let t=[],n=[],r=0;for(let i of e){let e=W(i);n.length>0&&r+e>48e3&&(t.push(n),n=[],r=0),n.push(i),r+=e}return n.length>0&&t.push(n),t}async function K(e){return new Promise((n,i)=>{t(r(e),{level:6},(e,t)=>{if(e!==null){i(e);return}n(t)})})}const q={mode:`off`,recording:!1,getSessionId:()=>``,flush:async()=>Promise.resolve(),stop:async()=>Promise.resolve()},J=`lf_session_replay_mode`;function ae(e){if(typeof window>`u`||typeof document>`u`)return q;let t=oe(e),n=new F({idleTimeoutMs:t.sessionIdleTimeoutMs,maxDurationMs:t.maxSessionDurationMs,now:t.now}),r=e=>{let r=t.getSessionId?.();return r!==void 0&&r.length>0?r:e?n.touch().id:n.getSession().id},i=r(!1),a=I(),o=se(t,i,a);if(o===`off`)return q;let c=new B(t,i,o),l={},u=ne({emit:e=>{let t=r(!0);c.rotate(t)&&l.current?.takeFullSnapshot(),c.add(e)},maskAllInputs:t.maskAllInputs,maskTextSelector:t.maskTextSelector,blockSelector:t.blockSelector,checkoutEveryNms:o===`buffer`?12e4:0});l.current=u;let d,f=t.getTraceContext===void 0?void 0:setInterval(()=>{let e=t.getTraceContext?.(),n=e?.traceId;n!==void 0&&n.length>0&&n!==d&&(d=n,u.addCustomEvent(s.Trace,{traceId:n,spanId:e?.spanId}))},1e3),g=e=>{u.addCustomEvent(s.Error,e),c.getMode()===`buffer`&&(Y(a,r(!1),`full`),Q(c.triggerFlush(),t.onError))},_=e=>{g($(e.message,e.filename,X(e.error)))},v=e=>{let t=e.reason;g($(t?.message??String(e.reason??`unhandledrejection`),void 0,t?.stack))},y=()=>{document.visibilityState===`hidden`&&Q(c.flush({keepalive:!0}),t.onError)};window.addEventListener(`error`,_,!0),window.addEventListener(`unhandledrejection`,v,!0),document.addEventListener(`visibilitychange`,y),window.addEventListener(`pagehide`,y);let b=(e,t)=>{u.addCustomEvent(e,t)},x=t.captureConsole?p(b,{onError:t.onError}):Z,S=t.captureNetwork?m(b,{ignoreUrlPatterns:t.ignoreUrlPatterns,now:t.now,onError:t.onError,redactUrlPatterns:t.redactUrlPatterns}):Z,C=t.captureNavigation?h(b,{onError:t.onError}):Z;c.start();let w;return{get mode(){return c.getMode()},recording:!0,getSessionId:()=>r(!1),flush:async()=>c.flush(),stop:async()=>(w??=(async()=>{f!==void 0&&clearInterval(f),window.removeEventListener(`error`,_,!0),window.removeEventListener(`unhandledrejection`,v,!0),document.removeEventListener(`visibilitychange`,y),window.removeEventListener(`pagehide`,y),x(),S(),C(),u.stop(),await c.shutdown({keepalive:!1})})(),w)}}function oe(e){if(e.replayUrl.length===0)throw Error("logfire session replay: `replayUrl` is required");let t=e.fetchImpl??(typeof fetch==`function`?fetch.bind(globalThis):void 0);if(t===void 0)throw Error("logfire session replay: no `fetch` available; pass `fetchImpl`");return{replayUrl:e.replayUrl,headers:e.headers,token:e.token,getSessionId:e.getSessionId,sessionSampleRate:e.sessionSampleRate??l.sessionSampleRate,onErrorSampleRate:e.onErrorSampleRate??l.onErrorSampleRate,maskAllInputs:e.maskAllInputs??l.maskAllInputs,maskTextSelector:e.maskTextSelector??l.maskTextSelector,blockSelector:e.blockSelector??l.blockSelector,flushIntervalMs:e.flushIntervalMs??l.flushIntervalMs,maxBufferBytes:e.maxBufferBytes??l.maxBufferBytes,sessionIdleTimeoutMs:e.sessionIdleTimeoutMs??l.sessionIdleTimeoutMs,maxSessionDurationMs:e.maxSessionDurationMs??l.maxSessionDurationMs,distinctId:e.distinctId??l.distinctId,getDistinctId:e.getDistinctId,getTraceContext:e.getTraceContext,captureConsole:e.captureConsole??l.captureConsole,captureNetwork:e.captureNetwork??l.captureNetwork,captureNavigation:e.captureNavigation??l.captureNavigation,ignoreUrlPatterns:e.ignoreUrlPatterns??[],redactUrlPatterns:e.redactUrlPatterns??[],onError:e.onError,fetchImpl:t,now:e.now??Date.now,random:e.random??Math.random}}function se(e,t,n){let r=ce(n,t);if(r!==void 0)return r;let i=j({sessionSampleRate:e.sessionSampleRate,onErrorSampleRate:e.onErrorSampleRate,random:e.random});return Y(n,t,i),i}function ce(e,t){if(e!==null)try{let n=e.getItem(J);if(n===null)return;let r=JSON.parse(n);if(typeof r!=`object`||!r)return;let i=r;return i.id!==t||!le(i.mode)?void 0:i.mode}catch{return}}function Y(e,t,n){if(e!==null)try{e.setItem(J,JSON.stringify({id:t,mode:n}))}catch{}}function le(e){return e===`full`||e===`buffer`||e===`off`}function X(e){let t=typeof e==`object`&&e&&`stack`in e?e.stack:void 0;return typeof t==`string`?t:void 0}function Z(){}function Q(e,t){e.catch(e=>{t?.(e)})}function $(e,t,n){return{message:e,...t===void 0||t.length===0?{}:{source:t},...n===void 0?{}:{stack:n}}}export{c as CHUNK_ENVELOPE_VERSION,s as CustomTag,i as EventType,a as IncrementalSource,o as MouseInteractions,ae as startSessionReplay};
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@pydantic/logfire-session-replay",
3
+ "description": "Browser session replay recorder for Logfire - https://pydantic.dev/logfire",
4
+ "author": {
5
+ "name": "The Pydantic Team",
6
+ "email": "engineering@pydantic.dev",
7
+ "url": "https://pydantic.dev"
8
+ },
9
+ "repository": {
10
+ "url": "https://github.com/pydantic/logfire-js",
11
+ "directory": "packages/logfire-session-replay"
12
+ },
13
+ "sideEffects": false,
14
+ "homepage": "https://pydantic.dev/logfire",
15
+ "license": "MIT",
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "keywords": [
20
+ "logfire",
21
+ "observability",
22
+ "session-replay",
23
+ "browser",
24
+ "rum",
25
+ "rrweb"
26
+ ],
27
+ "version": "0.1.0-alpha.0",
28
+ "type": "module",
29
+ "main": "./dist/index.cjs",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "import": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "require": {
39
+ "types": "./dist/index.d.cts",
40
+ "default": "./dist/index.cjs"
41
+ }
42
+ }
43
+ },
44
+ "scripts": {
45
+ "dev": "vp pack --watch",
46
+ "build": "vp pack",
47
+ "lint": "vp lint",
48
+ "preview": "vp preview",
49
+ "typecheck": "tsc",
50
+ "prepack": "cp ../../LICENSE .",
51
+ "postpublish": "rm LICENSE",
52
+ "test": "vp test"
53
+ },
54
+ "dependencies": {
55
+ "fflate": "catalog:",
56
+ "rrweb": "catalog:"
57
+ },
58
+ "devDependencies": {
59
+ "jsdom": "catalog:",
60
+ "vitest": "catalog:"
61
+ },
62
+ "files": [
63
+ "dist",
64
+ "LICENSE"
65
+ ]
66
+ }