@glassanalytics/browser
Advanced tools
+20
-2
@@ -24,2 +24,14 @@ import { Value, FlagDefinition } from '@glassanalytics/core'; | ||
| captureConsole?: boolean; | ||
| /** | ||
| * Record console output INTO the session replay (surfaced in the player's | ||
| * Console inspector). Independent of `captureConsole` (which turns errors into | ||
| * Glass events). Default on when replay is on. | ||
| */ | ||
| recordConsole?: boolean; | ||
| /** | ||
| * Record network requests (fetch/XHR) into the session replay (player's Network | ||
| * inspector). Captures ONLY method, URL, status and timing — never headers or | ||
| * bodies — and never the Glass ingest host itself. Default on when replay is on. | ||
| */ | ||
| recordNetwork?: boolean; | ||
| /** Fraction of sessions recorded (client-side). */ | ||
@@ -30,2 +42,8 @@ sampleRate?: number; | ||
| maskTextSelector?: string; | ||
| /** | ||
| * Extra CSS selector for subtrees to exclude from replay (shown as a | ||
| * placeholder). Glass ALWAYS also blocks `.glass-block` / `[data-glass-block]` | ||
| * (and rrweb's `rr-block`) — mark decorative/animated elements with those to | ||
| * keep them out of recordings without flooding the stream. | ||
| */ | ||
| blockSelector?: string; | ||
@@ -41,3 +59,3 @@ respectDoNotTrack?: boolean; | ||
| * Optional flag DEFINITIONS for local (offline / property-aware) bucketing via | ||
| * `@glassanalytics/core` evaluateFlag. The hosted server intentionally does NOT leak the | ||
| * `@glass/core` evaluateFlag. The hosted server intentionally does NOT leak the | ||
| * rule set, so this is for self-host / advanced setups that ship definitions. | ||
@@ -153,3 +171,3 @@ */ | ||
| private resolve; | ||
| /** Local bucketing via @glassanalytics/core when definitions were provided to init(). */ | ||
| /** Local bucketing via @glass/core when definitions were provided to init(). */ | ||
| private evaluateLocal; | ||
@@ -156,0 +174,0 @@ isFeatureEnabledSync(key: string, opts?: FlagOptions): boolean; |
+138
-3
@@ -122,5 +122,104 @@ import { randomToken, prefixedId, evaluateFlag, INGEST_AUTH_SCHEME, replaySegmentPath } from '@glassanalytics/core'; | ||
| // src/network-plugin.ts | ||
| var NETWORK_PLUGIN_NAME = "rrweb/network@1"; | ||
| function getGlassNetworkPlugin(options = {}) { | ||
| return { | ||
| name: NETWORK_PLUGIN_NAME, | ||
| options, | ||
| observer(cb, _win, opts) { | ||
| if (typeof window === "undefined") return () => { | ||
| }; | ||
| const ignore = opts.ignoreUrlPrefix; | ||
| const teardowns = []; | ||
| const shouldIgnore = (url) => !url || (ignore ? url.startsWith(ignore) : false); | ||
| const record = (r) => { | ||
| try { | ||
| cb({ requests: [r] }); | ||
| } catch { | ||
| } | ||
| }; | ||
| if (typeof window.fetch === "function") { | ||
| const orig = window.fetch; | ||
| const patched = (input, init) => { | ||
| const url = typeof input === "string" ? input : input instanceof URL ? input.href : input?.url ?? ""; | ||
| const method = (init?.method ?? (typeof input === "object" && input && "method" in input ? input.method : "GET")).toUpperCase(); | ||
| const startTime = Date.now(); | ||
| const p = orig(input, init); | ||
| if (!shouldIgnore(url)) { | ||
| p.then( | ||
| (res) => record({ type: "fetch", method, name: url, status: res.status, startTime, endTime: Date.now() }), | ||
| () => record({ type: "fetch", method, name: url, status: 0, startTime, endTime: Date.now() }) | ||
| ); | ||
| } | ||
| return p; | ||
| }; | ||
| window.fetch = patched; | ||
| teardowns.push(() => { | ||
| window.fetch = orig; | ||
| }); | ||
| } | ||
| if (typeof XMLHttpRequest !== "undefined") { | ||
| const proto = XMLHttpRequest.prototype; | ||
| const origOpen = proto.open; | ||
| const origSend = proto.send; | ||
| const meta = /* @__PURE__ */ new WeakMap(); | ||
| proto.open = function open(method, url, ...rest) { | ||
| meta.set(this, { | ||
| method: String(method ?? "GET").toUpperCase(), | ||
| url: typeof url === "string" ? url : url.href | ||
| }); | ||
| return origOpen.call( | ||
| this, | ||
| method, | ||
| url, | ||
| ...rest | ||
| ); | ||
| }; | ||
| proto.send = function send(...args) { | ||
| const m = meta.get(this); | ||
| if (m && !shouldIgnore(m.url)) { | ||
| const startTime = Date.now(); | ||
| this.addEventListener( | ||
| "loadend", | ||
| () => record({ | ||
| type: "xhr", | ||
| method: m.method, | ||
| name: m.url, | ||
| status: this.status, | ||
| startTime, | ||
| endTime: Date.now() | ||
| }), | ||
| { once: true } | ||
| ); | ||
| } | ||
| return origSend.apply(this, args); | ||
| }; | ||
| teardowns.push(() => { | ||
| proto.open = origOpen; | ||
| proto.send = origSend; | ||
| }); | ||
| } | ||
| return () => { | ||
| for (const t of teardowns) { | ||
| try { | ||
| t(); | ||
| } catch { | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| }; | ||
| } | ||
| // src/replay.ts | ||
| var SENSITIVE_INPUT_TYPES = /* @__PURE__ */ new Set(["password", "email", "tel", "hidden"]); | ||
| var SENSITIVE_NAME_RE = /pass|secret|token|ssn|card|cvv|cvc|account|email|phone|tax/i; | ||
| var GLASS_BLOCK_SELECTOR = ".glass-block,[data-glass-block]"; | ||
| var SAMPLING = { | ||
| mousemove: 50, | ||
| mouseInteraction: true, | ||
| scroll: 150, | ||
| media: 800, | ||
| input: "last" | ||
| }; | ||
| var ReplayRecorder = class { | ||
@@ -141,2 +240,3 @@ constructor(cfg, getSessionId) { | ||
| addCustom = null; | ||
| boundVisibility = null; | ||
| async start() { | ||
@@ -148,2 +248,13 @@ if (this.stop) return; | ||
| this.addCustom = typeof rec.addCustomEvent === "function" ? rec.addCustomEvent.bind(record) : null; | ||
| const plugins = []; | ||
| if (this.cfg.recordConsole) { | ||
| try { | ||
| const { getRecordConsolePlugin } = await import('@rrweb/rrweb-plugin-console-record'); | ||
| plugins.push(getRecordConsolePlugin()); | ||
| } catch { | ||
| } | ||
| } | ||
| if (this.cfg.recordNetwork) { | ||
| plugins.push(getGlassNetworkPlugin({ ignoreUrlPrefix: this.cfg.ingestHost })); | ||
| } | ||
| this.stop = record({ | ||
@@ -154,6 +265,9 @@ emit: (event) => { | ||
| }, | ||
| plugins: plugins.length ? plugins : void 0, | ||
| // Privacy defaults: never leak user input. | ||
| maskAllInputs: this.cfg.maskAllInputs, | ||
| maskTextSelector: this.cfg.maskTextSelector, | ||
| blockSelector: this.cfg.blockSelector, | ||
| // Merge the caller's blockSelector with Glass's always-on block convention. | ||
| blockSelector: this.cfg.blockSelector ? `${this.cfg.blockSelector},${GLASS_BLOCK_SELECTOR}` : GLASS_BLOCK_SELECTOR, | ||
| sampling: SAMPLING, | ||
| // ALWAYS mask sensitive fields regardless of maskAllInputs (defense in depth). | ||
@@ -173,2 +287,11 @@ maskInputFn: (text, element) => { | ||
| window.addEventListener("pagehide", this.boundFinal); | ||
| if (typeof document !== "undefined") { | ||
| this.boundVisibility = () => { | ||
| const hidden = document.visibilityState === "hidden"; | ||
| this.addEvent(hidden ? "$window_hidden" : "$window_visible", { | ||
| event: hidden ? "Window became hidden" : "Window became visible" | ||
| }); | ||
| }; | ||
| document.addEventListener("visibilitychange", this.boundVisibility); | ||
| } | ||
| } | ||
@@ -233,2 +356,6 @@ /** | ||
| } | ||
| if (this.boundVisibility && typeof document !== "undefined") { | ||
| document.removeEventListener("visibilitychange", this.boundVisibility); | ||
| this.boundVisibility = null; | ||
| } | ||
| this.encoder.dispose(); | ||
@@ -586,2 +713,4 @@ } | ||
| captureConsole: config.captureConsole ?? false, | ||
| recordConsole: config.recordConsole ?? true, | ||
| recordNetwork: config.recordNetwork ?? true, | ||
| sampleRate: config.sampleRate ?? 1, | ||
@@ -774,3 +903,9 @@ consent: config.consent ?? "implied", | ||
| const name = event ?? type; | ||
| this.replay.addEvent(name, { event: name, ...type === "error" ? { level: "error" } : {} }); | ||
| this.replay.addEvent(name, { | ||
| event: name, | ||
| ...type === "error" ? { level: "error" } : {}, | ||
| // Stamp the URL on pageviews so the player's address bar follows SPA route | ||
| // changes — rrweb only emits a meta snapshot on full (hard) page loads. | ||
| ...name === "$pageview" && typeof location !== "undefined" ? { href: location.href } : {} | ||
| }); | ||
| } | ||
@@ -984,3 +1119,3 @@ } | ||
| } | ||
| /** Local bucketing via @glassanalytics/core when definitions were provided to init(). */ | ||
| /** Local bucketing via @glass/core when definitions were provided to init(). */ | ||
| evaluateLocal(key) { | ||
@@ -987,0 +1122,0 @@ const def = this.cfg.flagDefinitions.find((d) => d.key === key); |
+2
-13
| { | ||
| "name": "@glassanalytics/browser", | ||
| "version": "0.1.2", | ||
| "version": "0.1.4", | ||
| "description": "Glass browser SDK — session replay (rrweb), autocapture, identity, errors and edge-evaluated feature flags. Small, lazy, safe-by-default.", | ||
@@ -18,18 +18,7 @@ "license": "Apache-2.0", | ||
| "sideEffects": false, | ||
| "scripts": { | ||
| "build": "tsup", | ||
| "dev": "tsup --watch", | ||
| "typecheck": "tsc --noEmit", | ||
| "test": "vitest run --passWithNoTests", | ||
| "clean": "rm -rf dist .turbo" | ||
| }, | ||
| "dependencies": { | ||
| "@glassanalytics/core": "0.1.0", | ||
| "@rrweb/rrweb-plugin-console-record": "2.0.1", | ||
| "rrweb": "^2.0.0-alpha.18" | ||
| }, | ||
| "devDependencies": { | ||
| "tsup": "^8.3.5", | ||
| "typescript": "^5.7.2", | ||
| "vitest": "^2.1.8" | ||
| } | ||
| } |
Sorry, the diff of this file is too big to display
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
142475
14.88%0
-100%1372
12.46%3
50%3
50%7
133.33%+ Added